Экономика снастей: апгрейды id=0 ~÷3.5 (удочка 70/210/500, приманка 60/160/400), база id=1 ÷2 (875/625), база id=3 ×1.5 (15000/11250); апгрейды id>0 — по формуле (180/530/1050, 130/380/750; 175/125 → округление до 10). README: Озёра 25 000 → 100 000 (актуальность); AGENTS.md: цифры баланса + 117 проверок; скриншоты обновлены. 117/117, exit 0

This commit is contained in:
2026-09-07 10:07:12 +03:00
parent c1e0389e44
commit d5212e9b42
17 changed files with 31 additions and 31 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
npm install # puppeteer-core
npm test # = node test.js
```
Ждать все PASS и exit 0. Сейчас проверок 115.
Ждать все PASS и exit 0. Сейчас проверок 117.
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
```bash
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
@@ -29,7 +29,7 @@
## Где крутить
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта (базы ×5: удочки 1750/4500/10000, наживки 1250/3000/7500). Апгрейды снастей (уровни 1..4): цена перехода — `upgCost(type,i,lv)`: id 0 — фикс `ROD_UPG_COST`/`LURE_UPG_COST` (250/750/1800 и 200/550/1400), 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()`.
- Баланс (рыба/удилища/приманки): константы `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 — карточка улова только бейдж/оформление по варианту.
+1 -1
View File
@@ -33,7 +33,7 @@
- Варианты рыб: серебряная ×2, золотая ×3, радужная ×5 — бейдж и оформление карточки улова
- Погода: дождь (🌧️) — шанс серебряной/золотой/радужной ×2,5; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — ![дождь](screenshots/shot-weather-rain.png)
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
- Локации: Озёра (25 000 🪙) — судак, хариус, гигантский рак; свой состав днём/ночью, свой цвет воды; покупка и переключение во вкладке 🗺️ лавки — ![озёра](screenshots/shot-lake.png)
- Локации: Озёра (100 000 🪙) — судак, хариус, гигантский рак; свой состав днём/ночью, свой цвет воды; покупка и переключение во вкладке 🗺️ лавки — ![озёра](screenshots/shot-lake.png)
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 18 достижений с бонусами
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
- Магазин с вкладками: Снасти 🎣, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
+6 -6
View File
@@ -365,20 +365,20 @@ let forceWeather=null; // M8: тест-форс погоды ('rain'/'fog'/null
const RODS=[
{id:0, name:'Дедова удочка', icon:'🧹', cost:0, maxW:3, line:100, drag:34, reel:1.0, size:0.00, desc:'До 3 кг · Нагрузка 100 · Drag 34 · Подсечка ×1.0'},
{id:1, name:'Стеклопластик', icon:'🎣', cost:1750, maxW:8, line:140, drag:47, reel:1.3, size:0.08, desc:'До 8 кг · Нагрузка 140 · Drag 47 · Подсечка ×1.3'},
{id:1, name:'Стеклопластик', icon:'🎣', cost:875, maxW:8, line:140, drag:47, reel:1.3, size:0.08, desc:'До 8 кг · Нагрузка 140 · Drag 47 · Подсечка ×1.3'},
{id:2, name:'Карбоновая', icon:'🏹', cost:4500, maxW:18, line:190, drag:63, reel:1.7, size:0.16, desc:'До 18 кг · Нагрузка 190 · Drag 63 · Подсечка ×1.7'},
{id:3, name:'Титан', icon:'💪', cost:10000, maxW:40, line:260, drag:87, reel:2.3, size:0.28, desc:'До 40 кг · Нагрузка 260 · Drag 87 · Подсечка ×2.3'},
{id:3, name:'Титан', icon:'💪', cost:15000, maxW:40, line:260, drag:87, reel:2.3, size:0.28, desc:'До 40 кг · Нагрузка 260 · Drag 87 · Подсечка ×2.3'},
];
const LURES=[
{id:0, name:'Простой крючок', icon:'🪝', cost:0, cast:1.00, bite:1.0, rare:0.0, size:0.00, desc:'Надёжно, но рыба не в восторге'},
{id:1, name:'Червь', icon:'🪱', cost:1250, cast:1.00, bite:1.3, rare:0.2, size:0.12, desc:'Клёв на 30% чаще, шанс крупной выше'},
{id:1, name:'Червь', icon:'🪱', cost:625, cast:1.00, bite:1.3, rare:0.2, size:0.12, desc:'Клёв на 30% чаще, шанс крупной выше'},
{id:2, name:'Воблер', icon:'🐟', cost:3000, cast:1.15, bite:1.2, rare:0.8, size:0.28, desc:'Дальше бросок, приманивает крупных'},
{id:3, name:'Золотая блёстка',icon:'✨', cost:7500, cast:1.20, bite:1.4, rare:2.0, size:0.50, desc:'Лучшее, что есть. К ней плывут легенды'},
{id:3, name:'Золотая блёстка',icon:'✨', cost:11250, cast:1.20, bite:1.4, rare:2.0, size:0.50, desc:'Лучшее, что есть. К ней плывут легенды'},
];
/* M5: апгрейды снастей — уровни 1..4 (макс). id 0 (база 0) — фиксированные цены
переходов 1→2, 2→3, 3→4; снасти id>0 — доля UPG_MULT от их базовой стоимости */
const ROD_UPG_COST=[250,750,1800];
const LURE_UPG_COST=[200,550,1400];
const ROD_UPG_COST=[70,210,500];
const LURE_UPG_COST=[60,160,400];
const UPG_MULT=[0.2,0.6,1.2]; // доля базы: →2 ур ×0.2, →3 ур ×0.6, →4 ур ×1.2
/* Что даёт следующий уровень (индекс = текущий уровень − 1) — подпись в лавке */
const ROD_UPG_NEXT=['фонарик (ночь), леска ×1.15','тормоз ×1.15, катушка ×1.10','леска ещё ×1.15, крупные +0.10'];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 160 KiB

+22 -22
View File
@@ -191,11 +191,11 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
ok('у удилища указан предел веса', rodDesc.includes('кг'));
await page.click('#shopBody [data-act="buy"][data-type="rod"]');
const rodAfter = await page.evaluate(() => ({ rod: save.rod, coins: save.coins, owned: save.rods.length }));
ok('куплено удилище', rodAfter.rod === 1 && rodAfter.coins === 8250 && rodAfter.owned === 2, JSON.stringify(rodAfter));
ok('куплено удилище', rodAfter.rod === 1 && rodAfter.coins === 9125 && rodAfter.owned === 2, JSON.stringify(rodAfter));
await page.click('#shopBody [data-act="buy"][data-type="lure"]');
const lureAfter = await page.evaluate(() => ({ lure: save.lure, coins: save.coins }));
ok('куплена приманка', lureAfter.lure === 1 && lureAfter.coins === 7000, JSON.stringify(lureAfter));
ok('куплена приманка', lureAfter.lure === 1 && lureAfter.coins === 8500, JSON.stringify(lureAfter));
await page.click('#tabJournal');
await sleep(100);
@@ -210,7 +210,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.reload({ waitUntil: 'load' });
await sleep(500);
const saved = await page.evaluate(() => ({ coins: save.coins, rod: save.rod, lure: save.lure, casts: save.casts }));
ok('сохранение после reload', saved.coins === 7000 && saved.rod === 1 && saved.lure === 1 && saved.casts === 5, JSON.stringify(saved));
ok('сохранение после reload', saved.coins === 8500 && saved.rod === 1 && saved.lure === 1 && saved.casts === 5, JSON.stringify(saved));
// --- 9. Сброс данных (играть сначала нужно зайти в игру) ---
await page.click('#btnPlay');
@@ -222,7 +222,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.click('#btnResetNo');
ok('отмена: диалог закрыт, данные не тронуты',
await page.$eval('#resetDlg', el => el.classList.contains('hidden'))
&& (await page.evaluate(() => save.coins)) === 7000);
&& (await page.evaluate(() => save.coins)) === 8500);
await page.click('#btnReset');
await sleep(100);
await page.click('#btnResetYes');
@@ -565,8 +565,8 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const row=items.find(el=>el.querySelector('[data-act="upgrade"][data-type="rod"]'));
return row?{txt:row.textContent}:null;
});
ok('M5 лавка: фонарик = апгрейд 1 удилища (кнопка ⬆ 250 🪙, подпись «фонарик (ночь)»)',
m3shop && m3shop.txt.indexOf('250')>=0 && m3shop.txt.indexOf('фонарик')>=0 && m3shop.txt.indexOf('ноч')>=0,
ok('M5 лавка: фонарик = апгрейд 1 удилища (кнопка ⬆ 70 🪙, подпись «фонарик (ночь)»)',
m3shop && m3shop.txt.indexOf('70 🪙')>=0 && m3shop.txt.indexOf('фонарик')>=0 && m3shop.txt.indexOf('ноч')>=0,
m3shop?m3shop.txt:'нет строки апгрейда');
await page.click('#btnCloseShop');
@@ -1038,8 +1038,8 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
};
return { r1: info(byName('Стеклопластик')), l1: info(byName('Червь')) };
});
ok('M5 гейтинг лавки: после rodLv[0]=4 / lureLv[0]=4 — locked снят, кнопки buy «1750 🪙» / «1250 🪙»',
!m5g2.r1.locked && m5g2.r1.buyTxt === '1750 🪙' && !m5g2.l1.locked && m5g2.l1.buyTxt === '1250 🪙',
ok('M5 гейтинг лавки: после rodLv[0]=4 / lureLv[0]=4 — locked снят, кнопки buy «875 🪙» / «625 🪙»',
!m5g2.r1.locked && m5g2.r1.buyTxt === '875 🪙' && !m5g2.l1.locked && m5g2.l1.buyTxt === '625 🪙',
JSON.stringify(m5g2));
// (8) двойная защита: buy-ветка shopClick при закрытом гейтинге не покупает
@@ -1061,7 +1061,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
&& m5d.toast.includes('Сначала улучши'),
JSON.stringify(m5d));
// (9) покупка апгрейда: 1000 🪙 → 750, rodLv[0]=2, фонарик включился
// (9) покупка апгрейда: 1000 🪙 → 930, rodLv[0]=2, фонарик включился
await page.evaluate(() => {
save.rods = [0]; save.lures = [0]; save.rod = 0; save.lure = 0;
save.rodLv = { 0: 1 }; save.lureLv = { 0: 1 }; save.coins = 1000;
@@ -1072,8 +1072,8 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
coins: save.coins, lv: save.rodLv[0], light: hasLight(),
toast: document.getElementById('toast').textContent
}));
ok('M5 покупка апгрейда: rod 0 1→2 ур. за 250 🪙 (1000→750), hasLight()=true, тост «Улучшено … → 2 ур»',
m5u.coins === 750 && m5u.lv === 2 && m5u.light === true
ok('M5 покупка апгрейда: rod 0 1→2 ур. за 70 🪙 (1000→930), hasLight()=true, тост «Улучшено … → 2 ур»',
m5u.coins === 930 && m5u.lv === 2 && m5u.light === true
&& m5u.toast.includes('Улучшено') && m5u.toast.includes('2 ур'),
JSON.stringify(m5u));
@@ -1087,14 +1087,14 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
!m5x.hasUpg && m5x.txt.includes('●●●●') && m5x.txt.includes('4 ур.'), m5x.txt);
// (11) не хватает монет: апгрейд не покупается
await page.evaluate(() => { save.rodLv[0] = 1; save.coins = 100; renderShop(); });
await page.evaluate(() => { save.rodLv[0] = 1; save.coins = 50; renderShop(); });
await page.click('#shopBody [data-act="upgrade"][data-type="rod"][data-i="0"]');
const m5p = await page.evaluate(() => ({
coins: save.coins, lv: save.rodLv[0],
toast: document.getElementById('toast').textContent
}));
ok('M5 не хватает монет: 100 🪙 на апгрейд за 250 — coins=100, уровень не изменился, тост «Не хватает монет»',
m5p.coins === 100 && m5p.lv === 1 && m5p.toast.includes('Не хватает монет'),
ok('M5 не хватает монет: 50 🪙 на апгрейд за 70 — coins=50, уровень не изменился, тост «Не хватает монет»',
m5p.coins === 50 && m5p.lv === 1 && m5p.toast.includes('Не хватает монет'),
JSON.stringify(m5p));
await page.click('#btnCloseShop');
@@ -1146,15 +1146,15 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
rods: save.rods.join(','), lv: save.rodLv[1],
rod: save.rod, coins: save.coins
}));
ok('M5 покупка разблокированной снасти: rod 1 за 1750 🪙 — rods=[0,1], rodLv[1]=1, снасть экипирована, coins=1250',
m5b.rods === '0,1' && m5b.lv === 1 && m5b.rod === 1 && m5b.coins === 1250,
ok('M5 покупка разблокированной снасти: rod 1 за 875 🪙 — rods=[0,1], rodLv[1]=1, снасть экипирована, coins=2125',
m5b.rods === '0,1' && m5b.lv === 1 && m5b.rod === 1 && m5b.coins === 2125,
JSON.stringify(m5b));
// (14b) апгрейд по формуле: rod 1 (база 1750) 1→2 ур. = 1750×0.2 = 350
// (14b) апгрейд по формуле: rod 1 (база 875) 1→2 ур. = 875×0.2 = 175 → 180 (округление до 10)
await page.click('#shopBody [data-act="upgrade"][data-type="rod"][data-i="1"]');
const m5bu = await page.evaluate(() => ({ lv: save.rodLv[1], coins: save.coins }));
ok('M5 апгрейд по формуле: rod 1 1→2 ур. за 350 🪙 (база 1750×0.2) — coins=1250→900, rodLv[1]=2',
m5bu.lv === 2 && m5bu.coins === 900, JSON.stringify(m5bu));
ok('M5 апгрейд по формуле: rod 1 1→2 ур. за 180 🪙 (база 875×0.2 = 175 → округление до 10) — coins=2125→1945, rodLv[1]=2',
m5bu.lv === 2 && m5bu.coins === 1945, JSON.stringify(m5bu));
await page.click('#btnCloseShop');
// (15) пин цен апгрейдов: id 0 — фикс, id>0 — база×(0.2/0.6/1.2)
@@ -1162,9 +1162,9 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
r: [0,1,2,3].map(i=>[1,2,3].map(lv=>upgCost('rod',i,lv)).join(',')),
l: [0,1,2,3].map(i=>[1,2,3].map(lv=>upgCost('lure',i,lv)).join(','))
}));
ok('M5 цены апгрейдов: id 0 фикс (250/750/1800, 200/550/1400); id>0 = база×(0.2/0.6/1.2)',
m5c.r.join('|')==='250,750,1800|350,1050,2100|900,2700,5400|2000,6000,12000'
&& m5c.l.join('|')==='200,550,1400|250,750,1500|600,1800,3600|1500,4500,9000',
ok('M5 цены апгрейдов: id 0 фикс (70/210/500, 60/160/400); id>0 = база×(0.2/0.6/1.2)',
m5c.r.join('|')==='70,210,500|180,530,1050|900,2700,5400|3000,9000,18000'
&& m5c.l.join('|')==='60,160,400|130,380,750|600,1800,3600|2250,6750,13500',
JSON.stringify(m5c));
// --- 10c. M6: варианты рыб (VARIANTS, forceVariant, карточка, full_variants) ---