diff --git a/AGENTS.md b/AGENTS.md
index d657147..62c08a3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,7 +36,7 @@
- Баланс (рыба/удилища/приманки): константы `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` — 8 вариантов, порядок ролла = порядок массива: copper ×1.5 @0.12 → silver ×2 @0.08 → quartz ×2.5 @0.07 → gold ×3 @`SHINY_CHANCE` (0.06, живая) → emerald ×4 @0.01 → diamond ×4.5 @0.007 → rainbow ×5 @0.005 (топ), иначе normal; P(особой)≈31%, E[mult]≈1.36. `variantChance()` — единственная точка вероятностей (дождь M8 уже здесь — ×2,5, см. строку «Погода (M8)»), счётчики улова — `VARIANT_KEY` (cp/sv/qz/sn/em/dm/rb; sn = «shiny» = gold), миграция счётчиков в `loadSave()` (дефолт 0), тест-форс `forceVariant`, босс всегда normal. Рыбу нельзя улучшать: «Улучшить вариант» (M6) убрано в этапе 21 — карточка улова только бейдж/оформление по варианту.
+- Варианты рыб (M6): таблица `VARIANTS` — 8 вариантов, порядок ролла = порядок массива: copper ×1.5 @0.12 → silver ×2 @0.08 → quartz ×2.5 @0.07 → gold ×3 @`SHINY_CHANCE` (0.06, живая) → emerald ×4 @0.01 → diamond ×4.5 @0.007 → rainbow ×5 @0.005 (топ), иначе normal; P(особой)≈31% без буста снасти, E[mult]≈1.36. `variantChance()` — единственная точка вероятностей (дождь M8 уже здесь — ×2,5, см. строку «Погода (M8)»; буст снасти M6+ тоже здесь: `variantBoost()` = `lure().rare×0.15 + (уровень наживки−1)×0.05`, множитель ×(1+B·r), r — номер в лестнице 1..7 — редким больше; простая наживка 1 ур. → B=0; блёстка 4 ур. → B=0.6, rainbow ×5.2), счётчики улова — `VARIANT_KEY` (cp/sv/qz/sn/em/dm/rb; sn = «shiny» = gold), миграция счётчиков в `loadSave()` (дефолт 0), тест-форс `forceVariant`, босс всегда normal. Рыбу нельзя улучшать: «Улучшить вариант» (M6) убрано в этапе 21 — карточка улова только бейдж/оформление по варианту. Пины шансов в тестах (§10c(1)/(5), M8(2)) — при нейтральной снасти (снапшот/рестор `save.lure`/`lureLv` в eval).
- Погода (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 не менять — старые сейвы).
diff --git a/README.md b/README.md
index a91bbc2..87ba96d 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@
- Цикл дня и ночи (~5 мин на сутки): ночью своя таблица рыб (сом/щука/окунь активнее), ночью нужен 🪯 фонарик — апгрейд 1 удилища
- Апгрейды снастей: удилища и приманки улучшаются до 4 уровня (свет фонарика, крепость лески, тормоз, катушка, клёв, редкие и крупные рыбы); следующая снасть в лавке открывается после 4 уровня предыдущей
- Босс-рыба — Осётр-босс (шанс при поимке осётра): 3 фазы «рвёт и уходит», награда ×5, достижение «Босс-рыбак»
-- Варианты рыб (особая рыба ≈31%): медная ×1.5, серебряная ×2, кварцевая ×2.5, золотая ×3, изумрудная ×4, алмазная ×4.5, радужная ×5 (топ, самая редкая) — бейдж и оформление карточки улова
+- Варианты рыб (особая рыба ≈31%): медная ×1.5, серебряная ×2, кварцевая ×2.5, золотая ×3, изумрудная ×4, алмазная ×4.5, радужная ×5 (топ, самая редкая) — бейдж и оформление карточки улова; снасть усиливает шанс (тип наживки и её уровень — редким вариантам прибавка больше, топ-снасть ≈×5 к шансу радужной)
- Погода: дождь (🌧️) — шанс особых рыб ×2,5; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты —
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
- Локации: Озёра (100 000 🪙) — судак, хариус, гигантский рак; свой состав днём/ночью, свой цвет воды; покупка и переключение во вкладке 🗺️ лавки —
diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md
index 9a855ea..dbc8792 100644
--- a/docs/DEVLOG.md
+++ b/docs/DEVLOG.md
@@ -603,3 +603,17 @@ git add -A && git commit -m "что изменилось" && git push
**Тесты:** §10c(1) — пин 8 вариантов/множителей/шансов; (2) — +cp/qz/em/dm=0; (5) — новые границы ролла; (6) — миграция всех счётчиков; (7) — probe всех 8 вариантов (бейдж/класс); (8) — `full_variants` 8×8 (sum 0.16 прежний); M8(2) — ×2.5 для всех 7 особых (tolerance 1e-9); локации §lc10 — сетап с 8 счётчиками.
**Результат прогона:** `node tools/check.mjs` — OK; `cd tests && npm test` — **177/177 PASS, exit 0**.
+
+## Этап 31. Снасть усиливает шанс особых вариантов (M6+)
+
+**Задача:** необычные рыбы ещё чаще — тип наживки и её уровень апгрейда повышают шанс особых вариантов, причём более редкие получают прибавку больше.
+
+**Решения:**
+- **Формула в `variantChance()`** (единственная точка вероятностей, как дождь M8): `variantBoost()` = `lure().rare×0.15 + (уровень наживки−1)×0.05`; множитель шанса варианта — **×(1+B·r)**, r — номер варианта в лестнице `VARIANTS` (медь 1 … радуга 7) → чем реже вариант, тем сильнее буст. Дождь ×2,5 применяется к уже усиленному шансу.
+- **Источник B — существующий rare-стат наживки** (`effLure().rare`: простой крючок 0 / червь 0.2 / воблер 0.8 / блёстка 2.0, +1.0 на 3–4 ур.) — и «тип», и «уровень» наживки работают через один статики; уровень апгрейда добавляет своё слагаемое (+0.05/ур.) — буст есть на любом уровне, не только на 3-м.
+- **Диапазон:** простая наживка 1 ур. → B=0 (шансы базовые, P(особой)≈31%); червь 1 ур. → B=0.03; воблер 1 ур. → B=0.12; блёстка 1 ур. → B=0.3 (rainbow ×3.1); блёстка 4 ур. → B=0.6 (rainbow ×5.2, gold ×3.4, P(особой)≈60%). Насыщения нет: при топ-снасти + дожде все шансы <1.
+- Пины шансов в тестах (§10c(1)/(5), M8(2)) переведены на нейтральную снасть: снапшот `save.lure`/`lureLv` в начале eval, рестор в конце (снасть mid-сьюта после M5-тестов непредсказуема).
+
+**Тесты:** +1 (§10c(9) «буст снасти», 178/178): B=0 у простой наживки; червь B=0.03 → copper 0.1236/rainbow 0.00605; блёстка B=0.3 → copper 0.156/gold 0.132/rainbow 0.0155; 4 ур. (rare 3.0) B=0.6 → rainbow 0.026; отношение множителей «радуга/медь» растёт с B (редким больше); ролл r=0.12 с червя → copper (граница под бустом); §10c(1) — +пин boost0=0.
+
+**Результат прогона:** `node tools/check.mjs` — OK; `cd tests && npm test` — **178/178 PASS, exit 0**.
diff --git a/index.html b/index.html
index 4d88679..d255e23 100644
--- a/index.html
+++ b/index.html
@@ -403,7 +403,8 @@ let SHINY_CHANCE=0.06;
/* M6: варианты рыб — таблица (порядок = лестница ценности и порядок ролла).
Ролл по порядку: copper → silver → quartz → gold (SHINY_CHANCE) → emerald → diamond → rainbow, иначе normal.
- P(особой)≈31%, E[mult]≈1.36; топ лестницы — радужная (×5, самая редкая) */
+ P(особой)≈31% без буста снасти, E[mult]≈1.36; топ лестницы — радужная (×5, самая редкая).
+ Снасть (тип наживки + её уровень) усиливает шансы — см. variantBoost()/variantChance() */
const VARIANTS=[
{id:'normal', name:'Обычная', nameEn:'Normal', mult:1, chance:0, css:''},
{id:'copper', name:'Медная', nameEn:'Copper', mult:1.5, chance:0.12, css:'copper'},
@@ -417,10 +418,20 @@ const VARIANTS=[
let forceVariant=null; // M6: тест-форс варианта (id из VARIANTS/null — всегда даёт его, паттерн forceBoss)
/* M6: счётчики вариантов в caught[id] (normal — без счётчика; sn — «shiny» = gold) */
const VARIANT_KEY={copper:'cp',silver:'sv',quartz:'qz',gold:'sn',emerald:'em',diamond:'dm',rainbow:'rb'};
+/* M6+: буст вариантов от снасти — тип наживки (её эффективный rare-стат: 0/0.2/0.8/2.0,
+ у 3–4 ур. +1.0) и уровень апгрейда наживки (1..4). Простая наживка 1 ур. — B=0 (шансы базовые) */
+function variantBoost(){
+ return lure().rare*0.15+(lureLvOf(save.lure)-1)*0.05;
+}
function variantChance(v){
let c=v.id==='gold'?SHINY_CHANCE:v.chance; // chance gold — живая SHINY_CHANCE
- // M8: дождь — особые варианты ярче (×2,5; normal не трогаем; босс всегда normal — не доходит сюда)
- if(v.id!=='normal'&&weatherActive()==='rain') c*=WEATHER.rain.variantMult;
+ if(v.id!=='normal'){
+ // M8: дождь — особые варианты ярче (×2,5; босс всегда normal — не доходит сюда)
+ if(weatherActive()==='rain') c*=WEATHER.rain.variantMult;
+ // M6+: снасть усиливает варианты — множитель ×(1+B·r), r — номер в лестнице VARIANTS (1..7):
+ // чем реже вариант, тем сильнее прибавка (медь ×(1+B) … радуга ×(1+7B))
+ c*=1+variantBoost()*VARIANTS.indexOf(v);
+ }
return c;
}
/* M6: ролл варианта улова */
diff --git a/tests/test.js b/tests/test.js
index b1d6b84..d3fe8ee 100644
--- a/tests/test.js
+++ b/tests/test.js
@@ -1302,8 +1302,11 @@ async function toGameMenu(p) {
JSON.stringify(m5c));
// --- 10c. M6: варианты рыб (VARIANTS, forceVariant, карточка, full_variants) ---
- // (1) данные: таблица VARIANTS и живой gold-шанс (variantChance → SHINY_CHANCE)
+ // (1) данные: таблица VARIANTS и живой gold-шанс (variantChance → SHINY_CHANCE);
+ // снасть — нейтральная (простая наживка 1 ур.): буст вариантов B=0, шансы базовые
const m6v = await page.evaluate(() => {
+ const snap = { lure: save.lure, lv: Object.assign({}, save.lureLv) };
+ save.lure = 0; save.lureLv = { 0: 1 };
SHINY_CHANCE = 0.07;
const out = {
n: VARIANTS.length,
@@ -1314,15 +1317,17 @@ async function toGameMenu(p) {
emerald: VARIANTS.find(v => v.id === 'emerald').chance,
diamond: VARIANTS.find(v => v.id === 'diamond').chance,
rainbow: VARIANTS.find(v => v.id === 'rainbow').chance,
- goldLive: variantChance(VARIANTS.find(v => v.id === 'gold'))
+ goldLive: variantChance(VARIANTS.find(v => v.id === 'gold')),
+ boost0: variantBoost()
};
SHINY_CHANCE = 0.06;
+ save.lure = snap.lure; save.lureLv = snap.lv;
return out;
});
- ok('M6 данные: VARIANTS 8 (mult 1/1.5/2/2.5/3/4/4.5/5), chance медь 0.12 / серебро 0.08 / кварц 0.07 / изумруд 0.01 / алмаз 0.007 / радуга 0.005, gold = живая SHINY_CHANCE (0.07)',
+ ok('M6 данные: VARIANTS 8 (mult 1/1.5/2/2.5/3/4/4.5/5), chance медь 0.12 / серебро 0.08 / кварц 0.07 / изумруд 0.01 / алмаз 0.007 / радуга 0.005, gold = живая SHINY_CHANCE (0.07); буст снасти у простой наживки 1 ур. = 0',
m6v.n === 8 && m6v.mults === '1,1.5,2,2.5,3,4,4.5,5' && m6v.copper === 0.12 && m6v.silver === 0.08
&& m6v.quartz === 0.07 && m6v.emerald === 0.01 && m6v.diamond === 0.007 && m6v.rainbow === 0.005
- && m6v.goldLive === 0.07,
+ && m6v.goldLive === 0.07 && m6v.boost0 === 0,
JSON.stringify(m6v));
// (2) forceVariant='silver': улов ×2, счётчик sv, бейдж и класс карточки
@@ -1462,6 +1467,8 @@ async function toGameMenu(p) {
const mr = Math.random;
forceVariant = null;
forceWeather = null; weather = null; // M8: дождь ×2.5 сдвинул бы границы — исключаем
+ const snap = { lure: save.lure, lv: Object.assign({}, save.lureLv) };
+ save.lure = 0; save.lureLv = { 0: 1 }; // M6+: нейтральная снасть — буст 0, границы базовые
SHINY_CHANCE = 0.06;
const seq = arr => { let i = 0; Math.random = () => arr[Math.min(i++, arr.length - 1)]; };
const out = {
@@ -1477,6 +1484,7 @@ async function toGameMenu(p) {
normal: (seq([0.9]), rollVariant().id) // все выше порогов → normal
};
Math.random = mr;
+ save.lure = snap.lure; save.lureLv = snap.lv;
return out;
});
ok('M6 границы ролла: r<0.12 → copper; [≥0.12, <0.08] → silver; [.., .., <0.07] → quartz; [.., .., .., <0.06] → gold; +emerald 0.01 / diamond 0.007 / rainbow 0.005; иначе → normal',
@@ -1564,6 +1572,51 @@ async function toGameMenu(p) {
&& m6fv.a2.coins === m6fv.a1.coins,
JSON.stringify(m6fv));
+ // (9) буст снасти (M6+): тип наживки (rare-стат) и её уровень повышают шанс особых вариантов,
+ // более редкие — сильнее (множитель ×(1+B·r), r — номер в лестнице); простая наживка 1 ур. — B=0
+ const m6boost = await page.evaluate(() => {
+ const snap = { lure: save.lure, lv: Object.assign({}, save.lureLv) };
+ forceWeather = null; weather = null;
+ SHINY_CHANCE = 0.06;
+ const ch = id => variantChance(VARIANTS.find(x => x.id === id));
+ const out = {};
+ save.lure = 0; save.lureLv = { 0: 1 };
+ out.hookB = variantBoost(); // простая наживка 1 ур. — 0
+ out.hookCopper = ch('copper'); // 0.12 — без буста
+ save.lure = 1; // червь rare 0.2 → B=0.03
+ out.wormB = variantBoost();
+ out.wormCopper = ch('copper'); // 0.12×1.03
+ out.wormRainbow = ch('rainbow'); // 0.005×1.21 — редким больше
+ out.wormRatio = out.wormRainbow / out.wormCopper / (0.005 / 0.12); // (1+7B)/(1+B) > 1
+ save.lure = 3; save.lureLv = { 0: 1, 3: 1 }; // блёстка rare 2.0 → B=0.3
+ out.spinB = variantBoost();
+ out.spinCopper = ch('copper'); // 0.12×1.3
+ out.spinGold = ch('gold'); // 0.06×2.2
+ out.spinRainbow = ch('rainbow'); // 0.005×3.1
+ out.spinRatio = out.spinRainbow / out.spinCopper / (0.005 / 0.12); // ≈2.38 — растёт с B
+ save.lureLv = { 0: 1, 3: 4 }; // + 4 ур. наживки → rare 3.0 (+1.0 от 3 ур.), B=0.6
+ out.spin4B = variantBoost();
+ out.spin4Rainbow = ch('rainbow'); // 0.005×5.2
+ // ролл с бустом: червь ловит медью r=0.12 (без буста — normal, см. §(5) notCopper)
+ const mr = Math.random; let i = 0; Math.random = () => [0.12][Math.min(i++, 0)];
+ forceVariant = null;
+ out.rollWorm = rollVariant().id; // copper
+ Math.random = mr;
+ save.lure = snap.lure; save.lureLv = snap.lv;
+ SHINY_CHANCE = 0.06;
+ return out;
+ });
+ ok('M6 буст снасти: простая 1 ур. B=0 (copper 0.12); червь B=0.03 — copper 0.1236, rainbow 0.00605 (редким больше); блёстка B=0.3 — copper 0.156, gold 0.132, rainbow 0.0155; 4 ур. (rare 3.0) B=0.6 — rainbow 0.026; отношение «радуга/медь» растёт с B; ролл r=0.12 с червя → copper',
+ m6boost.hookB === 0 && Math.abs(m6boost.hookCopper - 0.12) < 1e-9
+ && Math.abs(m6boost.wormB - 0.03) < 1e-9 && Math.abs(m6boost.wormCopper - 0.1236) < 1e-9
+ && Math.abs(m6boost.wormRainbow - 0.00605) < 1e-9 && m6boost.wormRatio > 1
+ && Math.abs(m6boost.spinB - 0.3) < 1e-9 && Math.abs(m6boost.spinCopper - 0.156) < 1e-9
+ && Math.abs(m6boost.spinGold - 0.132) < 1e-9 && Math.abs(m6boost.spinRainbow - 0.0155) < 1e-9
+ && m6boost.spinRatio > m6boost.wormRatio
+ && Math.abs(m6boost.spin4B - 0.6) < 1e-9 && Math.abs(m6boost.spin4Rainbow - 0.026) < 1e-9
+ && m6boost.rollWorm === 'copper',
+ JSON.stringify(m6boost));
+
// сброс тест-форсов
await page.evaluate(() => { forceBite = false; forceVariant = null; forceBoss = false; SHINY_CHANCE = 0.06; });
@@ -1581,11 +1634,14 @@ async function toGameMenu(p) {
// (2) дождь: все особые варианты ярче (×2.5)
const w2 = await page.evaluate(() => {
forceWeather = 'rain';
+ const snap = { lure: save.lure, lv: Object.assign({}, save.lureLv) };
+ save.lure = 0; save.lureLv = { 0: 1 }; // M6+: нейтральная снасть — буст 0, пины базовых шансов
SHINY_CHANCE = 0.06;
const ch = id => variantChance(VARIANTS.find(x => x.id === id));
const out = { cp: ch('copper'), s: ch('silver'), qz: ch('quartz'), g: ch('gold'),
em: ch('emerald'), dm: ch('diamond'), r: ch('rainbow') };
forceWeather = null;
+ save.lure = snap.lure; save.lureLv = snap.lv;
return out;
});
ok('M8 дождь: variantChance ×2.5 — copper 0.3, silver 0.2, quartz 0.175, gold 0.15 (SHINY_CHANCE 0.06), emerald 0.025, diamond 0.0175, rainbow 0.0125',