1133 lines
58 KiB
JavaScript
1133 lines
58 KiB
JavaScript
'use strict';
|
||
/* Автотест игры: прогон механик через CDP в headless Chromium */
|
||
const puppeteer = require('puppeteer-core');
|
||
const path = require('path');
|
||
const GAME_URL = 'file://' + path.resolve(__dirname, '..', 'index.html');
|
||
|
||
const results = [];
|
||
function ok(name, cond, extra = '') {
|
||
results.push({ name, pass: !!cond, extra });
|
||
console.log((cond ? 'PASS' : 'FAIL') + ' | ' + name + (extra ? ' | ' + extra : ''));
|
||
}
|
||
|
||
async function waitState(page, st, timeout = 8000) {
|
||
const t0 = Date.now();
|
||
let cur;
|
||
while (Date.now() - t0 < timeout) {
|
||
cur = await page.evaluate(() => state);
|
||
if (cur === st) return cur;
|
||
await new Promise(r => setTimeout(r, 40));
|
||
}
|
||
throw new Error('timeout waiting state "' + st + '", current=' + cur);
|
||
}
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
|
||
(async () => {
|
||
const browser = await puppeteer.launch({
|
||
executablePath: process.env.CHROMIUM || '/usr/bin/chromium-browser',
|
||
headless: true,
|
||
args: ['--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--mute-audio'],
|
||
defaultViewport: { width: 390, height: 844, deviceScaleFactor: 2 },
|
||
});
|
||
const page = await browser.newPage();
|
||
const consoleErrors = [];
|
||
const pageErrors = [];
|
||
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
||
page.on('pageerror', e => pageErrors.push(String(e)));
|
||
|
||
await page.goto(GAME_URL, { waitUntil: 'load' });
|
||
await sleep(600);
|
||
|
||
// --- 1. Меню ---
|
||
ok('меню показано', await page.$eval('#menu', el => !el.classList.contains('hidden')));
|
||
await page.screenshot({ path: 'shot-menu.png' });
|
||
|
||
// --- 2. Старт и бросок ---
|
||
await page.click('#btnPlay');
|
||
ok('старт: state=IDLE', (await page.evaluate(() => state)) === 'IDLE');
|
||
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down();
|
||
await sleep(700);
|
||
ok('зарядка: state=CHARGE', (await page.evaluate(() => state)) === 'CHARGE');
|
||
await page.screenshot({ path: 'shot-charge.png' });
|
||
await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
ok('бросок: state=WAIT', true);
|
||
await page.screenshot({ path: 'shot-wait.png' });
|
||
|
||
// --- 3. Клёв и подсечка ---
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
ok('клев: state=BITE', true);
|
||
await page.screenshot({ path: 'shot-bite.png' });
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
ok('подсечка: state=REEL', true);
|
||
ok('реель объекта создан', await page.evaluate(() => !!reel && reel.d > 0 && reel.d0 > 0));
|
||
|
||
// --- 4. Вываживание: поймали ---
|
||
await page.evaluate(() => { reel.d = 0.4; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
ok('ловля: state=CARD', true);
|
||
const cardInfo = await page.evaluate(() => ({
|
||
visible: !document.getElementById('card').classList.contains('hidden'),
|
||
name: document.getElementById('cardName').textContent,
|
||
sub: document.getElementById('cardSub').textContent,
|
||
val: document.getElementById('cardVal').textContent,
|
||
rec: !document.getElementById('cardRecord').classList.contains('hidden'),
|
||
coins: save.coins,
|
||
}));
|
||
ok('карточка улова показана', cardInfo.visible, JSON.stringify(cardInfo));
|
||
const wMatch = cardInfo.sub.match(/([\d.]+) кг/);
|
||
ok('на карточке есть вес рыбы', !!wMatch && cardInfo.sub.includes('🪙/кг'));
|
||
ok('первый улов вида = рекорд', cardInfo.rec);
|
||
await page.screenshot({ path: 'shot-card.png' });
|
||
await page.click('#btnCardOk');
|
||
ok('после карты: state=IDLE', (await page.evaluate(() => state)) === 'IDLE');
|
||
|
||
// --- 4b. Перегруз: тяжёлая рыба на слабой удочке рвёт шнур быстро ---
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
reel.w = 12; reel.ov = 12 / rod().maxW; reel.aggr = 1;
|
||
reel.d = reel.d0 = 150;
|
||
});
|
||
await page.mouse.down();
|
||
await waitState(page, 'SNAP', 6000);
|
||
await page.mouse.up();
|
||
await waitState(page, 'IDLE', 6000); // T2b: худший кейс (calm→pull) ≈ 2.3 с + SNAP 1 с
|
||
ok('перегруз: 12 кг на удочке 3 кг = быстрый обрыв', true);
|
||
|
||
// --- 4c. Рекорд: улов ниже лучшего — бейджа нет ---
|
||
await page.evaluate(() => { for (const s of SPECIES) save.caught[s.id] = { n: 1, best: 9999 }; });
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
ok('улов ниже рекорда: бейдж «РЕКОРД!» скрыт', await page.evaluate(() =>
|
||
document.getElementById('cardRecord').classList.contains('hidden')));
|
||
await page.click('#btnCardOk');
|
||
await sleep(100);
|
||
|
||
// --- 5. Бросок 2: обрыв линии ---
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => { reel.tension = rod().line - 0.3; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'SNAP', 3000).catch(() => {});
|
||
await page.mouse.up();
|
||
await waitState(page, 'IDLE', 3000);
|
||
ok('обрыв: SNAP -> IDLE', true);
|
||
|
||
// --- 6. Бросок 3: срыв рыбы (не тянем) ---
|
||
await page.mouse.down(); await sleep(600); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
reel.phase = 'pull'; reel.phaseT = 9999;
|
||
reel.d = reel.d0 * 1.38;
|
||
});
|
||
await waitState(page, 'WAIT', 15000);
|
||
ok('срыв: рыба ушла -> WAIT', true);
|
||
|
||
// --- 7. Магазин: покупка и экипировка ---
|
||
// M5: гейтинг — снасть №(i−1) должна быть на 4 ур.; уровни форсим, смысл
|
||
// проверки (купить 2-е удилище и 2-ю приманку) сохраняется
|
||
await page.evaluate(() => { save.coins = 10000; save.rodLv[0]=4; save.lureLv[0]=4; });
|
||
await page.click('#btnShop');
|
||
await sleep(150);
|
||
ok('магазин открыт', await page.$eval('#shop', el => !el.classList.contains('hidden')));
|
||
await page.screenshot({ path: 'shot-shop.png' });
|
||
|
||
const rodDesc = await page.$eval('#shopBody .item .itemdesc', el => el.textContent);
|
||
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 === 9750 && 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 === 9600, JSON.stringify(lureAfter));
|
||
|
||
await page.click('#tabJournal');
|
||
await sleep(100);
|
||
const jtxt = await page.$eval('#shopBody', el => el.textContent);
|
||
ok('дневник рендерится', jtxt.includes('Бросков') && jtxt.includes('🪙/кг'));
|
||
ok('дневник показывает пойманную рыбу с макс. весом', (jtxt.includes('Карась') || jtxt.includes('Окунь') || jtxt.includes('Лещ') || jtxt.includes('Карп') || jtxt.includes('Щука') || jtxt.includes('Сом') || jtxt.includes('Осётр') || jtxt.includes('Золотая рыбка')) && jtxt.includes('макс '));
|
||
await page.screenshot({ path: 'shot-journal.png' });
|
||
await page.click('#btnCloseShop');
|
||
ok('магазин закрыт', await page.$eval('#shop', el => el.classList.contains('hidden')));
|
||
|
||
// --- 8. Сохранение после перезагрузки ---
|
||
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 === 9600 && saved.rod === 1 && saved.lure === 1 && saved.casts === 5, JSON.stringify(saved));
|
||
|
||
// --- 9. Сброс данных (играть сначала нужно зайти в игру) ---
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
await page.click('#btnReset');
|
||
await sleep(150);
|
||
ok('диалог сброса открыт', await page.$eval('#resetDlg', el => !el.classList.contains('hidden')));
|
||
await page.screenshot({ path: 'shot-reset.png' });
|
||
await page.click('#btnResetNo');
|
||
ok('отмена: диалог закрыт, данные не тронуты',
|
||
await page.$eval('#resetDlg', el => el.classList.contains('hidden'))
|
||
&& (await page.evaluate(() => save.coins)) === 9600);
|
||
await page.click('#btnReset');
|
||
await sleep(100);
|
||
await page.click('#btnResetYes');
|
||
await sleep(150);
|
||
const afterReset = await page.evaluate(() => ({
|
||
coins: save.coins, rod: save.rod, lure: save.lure, casts: save.casts,
|
||
caught: JSON.stringify(save.caught), state: state,
|
||
menu: !document.getElementById('menu').classList.contains('hidden'),
|
||
ls: localStorage.getItem('fishing-save-v1'),
|
||
}));
|
||
ok('сброс: данные обнулены, возврат в меню',
|
||
afterReset.coins === 0 && afterReset.rod === 0 && afterReset.lure === 0
|
||
&& afterReset.casts === 0 && afterReset.caught === '{}' && afterReset.state === 'MENU'
|
||
&& afterReset.menu, JSON.stringify(afterReset));
|
||
ok('сброс: localStorage перезаписан', afterReset.ls === null || JSON.parse(afterReset.ls).coins === 0);
|
||
|
||
// --- 9a. Шини: форсированный шанс — монеты ×3, счётчик sn, бейдж в карточке ---
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
// M2-гвард: квесты и стрик не должны случайно выдать монеты и исказить точную проверку
|
||
await page.evaluate(() => {
|
||
save.quests = { date: todayStr(), list: [] };
|
||
save.streak = { last: todayStr(), days: 1, shieldWeek: null };
|
||
});
|
||
const coinsBeforeShiny = await page.evaluate(() => save.coins);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; SHINY_CHANCE = 1; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
window.__testW = reel.w; window.__testSpId = reel.sp.id;
|
||
reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5;
|
||
});
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
const shinyInfo = await page.evaluate(() => ({
|
||
w: window.__testW,
|
||
price: SPECIES.find(s => s.id === window.__testSpId).price,
|
||
val: parseInt(document.getElementById('cardVal').textContent.replace(/[^\d]/g, ''), 10),
|
||
coins: save.coins,
|
||
sn: save.caught[window.__testSpId].sn,
|
||
badgeHidden: document.getElementById('cardShiny').classList.contains('hidden'),
|
||
badgeText: document.getElementById('cardShiny').textContent,
|
||
goldCard: document.getElementById('card').classList.contains('shiny-card'),
|
||
}));
|
||
const shinyBase = shinyInfo.w * shinyInfo.price * 3;
|
||
ok('шини: монеты = вес×цена×3 (±5%), sn=1, бейдж «ШИНИ» в карточке',
|
||
shinyInfo.val >= Math.max(1, Math.round(shinyBase * 0.9))
|
||
&& shinyInfo.val <= Math.round(shinyBase * 1.1)
|
||
&& shinyInfo.coins === coinsBeforeShiny + shinyInfo.val
|
||
&& shinyInfo.sn === 1
|
||
&& !shinyInfo.badgeHidden && shinyInfo.badgeText.includes('ШИНИ')
|
||
&& shinyInfo.goldCard,
|
||
JSON.stringify(shinyInfo));
|
||
await page.click('#btnCardOk');
|
||
await page.evaluate(() => { SHINY_CHANCE = 0.03; });
|
||
|
||
// --- 9b. Миграция сейва: старый формат (число и {n,best} без sn) ---
|
||
await page.evaluate(() => {
|
||
localStorage.setItem('fishing-save-v1', JSON.stringify({
|
||
coins: 77, rod: 0, lure: 0, rods: [0], lures: [0], muted: false,
|
||
caught: { carass: { n: 2, best: 0.5 }, perch: 3 }, casts: 1, earned: 10
|
||
}));
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const mig = await page.evaluate(() => ({
|
||
menu: !document.getElementById('menu').classList.contains('hidden'),
|
||
coins: save.coins,
|
||
carass: save.caught.carass,
|
||
perch: save.caught.perch,
|
||
}));
|
||
ok('миграция: старый сейв читается (число → {n,best,sn:0}, без sn → sn=0), n/best на месте',
|
||
mig.menu && mig.coins === 77
|
||
&& mig.carass.n === 2 && mig.carass.best === 0.5 && mig.carass.sn === 0
|
||
&& mig.perch.n === 3 && mig.perch.best === 0 && mig.perch.sn === 0,
|
||
JSON.stringify(mig));
|
||
|
||
// --- 9c. Near-miss: сбежавшая рыба даёт 10% своей цены утешением ---
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
const coinsBeforeNm = await page.evaluate(() => save.coins);
|
||
await page.mouse.down(); await sleep(600); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
reel.w = 5; // фиксируем вес — утешение детерминированно
|
||
window.__testPrice = reel.sp.price;
|
||
reel.phase = 'pull'; reel.phaseT = 9999;
|
||
reel.d = reel.d0 * 1.38;
|
||
});
|
||
await waitState(page, 'WAIT', 15000);
|
||
const nm = await page.evaluate(() => ({
|
||
coins: save.coins,
|
||
exp: Math.floor(5 * window.__testPrice * 0.1),
|
||
toast: document.getElementById('toast').textContent,
|
||
}));
|
||
const nmDelta = nm.coins - coinsBeforeNm;
|
||
ok('near-miss: сбежавшая рыба даёт +~10% цены (диапазон)',
|
||
nm.exp > 0 && Math.abs(nmDelta - nm.exp) <= 1,
|
||
JSON.stringify(nm) + ' delta=' + nmDelta);
|
||
ok('near-miss: в DOM тост «Сбежала» с весом и +N 🪙',
|
||
nm.toast.startsWith('Сбежала!') && nm.toast.includes('кг') && nm.toast.includes('+') && nm.toast.includes('🪙'),
|
||
nm.toast);
|
||
|
||
// --- 9d. M2 квест: catch_weight (порог 0.1 кг) выполняется уловом, +reward ---
|
||
// после near-miss игра может быть «в воде» (WAIT) — выводим её в IDLE/МЕНЮ-старт
|
||
const m2start = await page.evaluate(() => state);
|
||
if(m2start === 'MENU'){
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
}else{
|
||
await page.click('#btnRecall');
|
||
await waitState(page, 'IDLE');
|
||
await page.mouse.move(195, 600); // клик утащил мышь на кнопку — возвращаем курсор на canvas
|
||
}
|
||
await page.evaluate(() => {
|
||
save.quests = { date: todayStr(), list: [{ tpl:'catch_weight', param:0.1, goal:1, prog:0, done:false, reward:120 }] };
|
||
save.streak = { last:todayStr(), days:3, shieldWeek:null }; // стрик уже «сегодня» — не мешает монетам
|
||
});
|
||
const m2qBefore = await page.evaluate(() => save.coins);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
const m2q = await page.evaluate(() => ({
|
||
done: save.quests.list[0].done,
|
||
val: parseInt(document.getElementById('cardVal').textContent.replace(/[^\d]/g, ''), 10),
|
||
coins: save.coins,
|
||
total: save.questsDoneTotal,
|
||
}));
|
||
ok('M2 квест: catch_weight выполнен уловом (+reward 120, questsDoneTotal=1)',
|
||
m2q.done===true && m2q.coins===m2qBefore+m2q.val+120 && m2q.total===1,
|
||
JSON.stringify(m2q));
|
||
await page.click('#btnCardOk');
|
||
await sleep(100);
|
||
|
||
// --- 9e. M2 новый день: date=вчера → перегенерация (сегодня, 3 квеста, prog=0, без повторов) ---
|
||
await page.evaluate(() => {
|
||
save.quests.date = dateShift(todayStr(), -1);
|
||
persist();
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m2d = await page.evaluate(() => ({
|
||
date: save.quests.date,
|
||
today: todayStr(),
|
||
n: save.quests.list.length,
|
||
progs: save.quests.list.map(q => q.prog),
|
||
uniq: new Set(save.quests.list.map(q => q.tpl)).size,
|
||
}));
|
||
ok('M2 новый день: квесты перегенерированы (сегодня, 3, prog=0, шаблоны без повторов)',
|
||
m2d.date===m2d.today && m2d.n===3 && m2d.progs.every(p=>p===0) && m2d.uniq===3,
|
||
JSON.stringify(m2d));
|
||
|
||
// --- 9f. M2 стрик: вчерашний визит → days+1 и награда дня (день 3 → +75) ---
|
||
const m2s3before = await page.evaluate(() => {
|
||
save.streak = { last: dateShift(todayStr(), -1), days:2, shieldWeek:null };
|
||
save.coins = 500;
|
||
persist();
|
||
return save.coins;
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m2s3 = await page.evaluate(() => ({ days:save.streak.days, last:save.streak.last, today:todayStr(), coins:save.coins }));
|
||
ok('M2 стрик: вчера → день 3, last=сегодня, +75 (награда дня 3)',
|
||
m2s3.days===3 && m2s3.last===m2s3.today && m2s3.coins===m2s3before+75,
|
||
JSON.stringify(m2s3));
|
||
|
||
// --- 9g. M2 щит стрика: пропуск в середине недели → days+1 + shieldWeek; повторный — days=1 ---
|
||
await page.evaluate(() => {
|
||
save.streak = { last: dateShift(todayStr(), -2), days:2, shieldWeek:null };
|
||
save.coins = 500;
|
||
persist();
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m2sh = await page.evaluate(() => ({ days:save.streak.days, sw:save.streak.shieldWeek, cur:isoWeek(todayStr()), coins:save.coins }));
|
||
ok('M2 стрик-щит: пропуск прощён — день 3, shieldWeek=текущая ISO-неделя, +75',
|
||
m2sh.days===3 && m2sh.sw===m2sh.cur && m2sh.coins===575,
|
||
JSON.stringify(m2sh));
|
||
await page.evaluate(() => {
|
||
save.streak = { last: dateShift(todayStr(), -2), days:2, shieldWeek:isoWeek(todayStr()) };
|
||
save.coins = 500;
|
||
persist();
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m2sh2 = await page.evaluate(() => ({ days:save.streak.days, coins:save.coins }));
|
||
ok('M2 стрик-щит контр: щит уже использован в неделе — день 1, +25',
|
||
m2sh2.days===1 && m2sh2.coins===525,
|
||
JSON.stringify(m2sh2));
|
||
|
||
// --- 9h. M2 достижения: achBonus и множитель монет ×1.01 ---
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
await page.evaluate(() => {
|
||
save.ach = { first_catch:true };
|
||
save.quests = { date:todayStr(), list:[] }; // квесты не должны мешать точной проверке
|
||
SHINY_CHANCE = 0; // исключаем случайный ×3
|
||
save.caught = {}; save.earned = 0; save.casts = 0; // чтобы за улов не выдалось другое достижение
|
||
save.coins = 500;
|
||
});
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
window.__m2W = reel.w; window.__m2Price = reel.sp.price;
|
||
reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5;
|
||
});
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
const m2a = await page.evaluate(() => ({
|
||
pct: achBonus().coinPct,
|
||
w: window.__m2W, price: window.__m2Price,
|
||
val: parseInt(document.getElementById('cardVal').textContent.replace(/[^\d]/g, ''), 10),
|
||
coins: save.coins,
|
||
}));
|
||
const m2base = m2a.w * m2a.price;
|
||
ok('M2 достижения: achBonus().coinPct=0.01 и монеты ×1.01 (диапазон ±5%)',
|
||
m2a.pct===0.01
|
||
&& m2a.val>=Math.max(1,Math.round(m2base*0.94))
|
||
&& m2a.val<=Math.round(m2base*1.08)
|
||
&& m2a.coins===500+m2a.val,
|
||
JSON.stringify(m2a));
|
||
await page.evaluate(() => { SHINY_CHANCE = 0.03; });
|
||
await page.click('#btnCardOk');
|
||
await sleep(100);
|
||
|
||
// --- 9i. M2 UI лавки: вкладки 🎯 (стрик+квесты) и 🏅 (достижения) ---
|
||
await page.evaluate(() => {
|
||
save.quests = { date:todayStr(), list:[{ tpl:'casts', param:0, goal:10, prog:3, done:false, reward:60 }] };
|
||
});
|
||
await page.click('#btnShop');
|
||
await sleep(150);
|
||
ok('M2 UI: вкладки «🎯» и «🏅» есть в лавке',
|
||
await page.$('#tabRewards') !== null && await page.$('#tabAch') !== null);
|
||
await page.click('#tabRewards');
|
||
await sleep(100);
|
||
const m2rw = await page.evaluate(() => document.getElementById('shopBody').textContent);
|
||
ok('M2 UI 🎯: стрик «День…» и квест с прогрессом 3/10',
|
||
m2rw.indexOf('День')>=0 && m2rw.indexOf('Сделай 10 забросов за день')>=0 && m2rw.indexOf('3/10')>=0,
|
||
m2rw.slice(0, 100));
|
||
await page.click('#tabAch');
|
||
await sleep(100);
|
||
const m2ac = await page.evaluate(() => ({
|
||
txt: document.getElementById('shopBody').textContent,
|
||
got: document.querySelector('#shopBody .jrow.got .jc') !== null,
|
||
}));
|
||
ok('M2 UI 🏅: список достижений, first_catch выдан с ✓',
|
||
m2ac.txt.indexOf('Первый улов')>=0 && m2ac.got===true,
|
||
m2ac.txt.slice(0, 100));
|
||
await page.click('#btnCloseShop');
|
||
|
||
// --- 9j. M3: цикл дня/ночи, ночная таблица, фонарик ---
|
||
// (a) фаза по dayTime
|
||
const m3ph = await page.evaluate(() => {
|
||
setDayTime(0.8); const n=dayPhase;
|
||
setDayTime(0.3); const d=dayPhase;
|
||
return { n, d };
|
||
});
|
||
ok('M3 цикл: dayTime 0.8 → night, 0.3 → day',
|
||
m3ph.n==='night' && m3ph.d==='day', JSON.stringify(m3ph));
|
||
|
||
// (b) ночная таблица: тот же ролл — другая рыба (веса 40 vs 100 у карася)
|
||
const m3tbl = await page.evaluate(() => {
|
||
save.lure=0; // rare=0 — веса таблицы без модификаторов
|
||
const mr=Math.random; Math.random=()=>0.25;
|
||
setDayTime(0.8); const night=pickSpecies(1).id;
|
||
setDayTime(0.3); const day=pickSpecies(1).id;
|
||
Math.random=mr;
|
||
return { night, day,
|
||
wN:SPECIES_NIGHT.find(s=>s.id==='carass').weight,
|
||
wD:SPECIES.find(s=>s.id==='carass').weight };
|
||
});
|
||
ok('M3 ночная таблица: ночью — SPECIES_NIGHT (карась 40 vs 100), тот же ролл → другая рыба',
|
||
m3tbl.night==='perch' && m3tbl.day==='carass' && m3tbl.wN===40 && m3tbl.wD===100,
|
||
JSON.stringify(m3tbl));
|
||
|
||
// (c) ночь без фонарика: клёва нет, тост разовый
|
||
await page.evaluate(() => {
|
||
setDayTime(0.8);
|
||
save.lures=[0]; save.lure=0;
|
||
save.quests={date:todayStr(),list:[]}; // чтобы квестный тост не затёр проверочный
|
||
state='IDLE'; biteFish=null;
|
||
});
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite=true; biteTimer=0.05; });
|
||
await sleep(600);
|
||
const m3nb = await page.evaluate(() => ({
|
||
st:state, bf:!!biteFish, bt:biteTimer,
|
||
toast:document.getElementById('toast').textContent
|
||
}));
|
||
ok('M3 ночь без фонарика: клёв не стартует (WAIT, нет biteFish, biteTimer тикает), тост «Ночью нужен фонарик»',
|
||
m3nb.st==='WAIT' && !m3nb.bf && m3nb.bt<0 && m3nb.toast.includes('Ночью нужен фонарик'),
|
||
JSON.stringify(m3nb));
|
||
|
||
// (d) ночь с фонариком: клёв работает (M5: фонарик = апгрейд 1 удилища)
|
||
await page.click('#btnRecall');
|
||
await waitState(page, 'IDLE');
|
||
await page.mouse.move(195, 600);
|
||
await page.evaluate(() => { setDayTime(0.8); save.rodLv[save.rod]=2; });
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite=true; biteTimer=0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
ok('M3 ночь с фонариком (удочка 2+ ур.: save.rodLv[save.rod]>=2): клёв работает (BITE)', true);
|
||
|
||
// (e) лавка: фонарик — не приманка, а апгрейд 1 удилища (кнопка ⬆ 400 🪙,
|
||
// в подписи «фонарик (ночь)»); уровень 1 форсим, чтобы строка показывала именно его
|
||
await page.click('#btnShop');
|
||
await sleep(150);
|
||
await page.click('#tabTackle');
|
||
await sleep(150);
|
||
const m3shop = await page.evaluate(() => {
|
||
save.rodLv[0]=1;
|
||
renderShop();
|
||
const items=Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||
const row=items.find(el=>el.querySelector('[data-act="upgrade"][data-type="rod"]'));
|
||
return row?{txt:row.textContent}:null;
|
||
});
|
||
ok('M5 лавка: фонарик = апгрейд 1 удилища (кнопка ⬆ 400 🪙, подпись «фонарик (ночь)»)',
|
||
m3shop && m3shop.txt.indexOf('400')>=0 && m3shop.txt.indexOf('фонарик')>=0 && m3shop.txt.indexOf('ноч')>=0,
|
||
m3shop?m3shop.txt:'нет строки апгрейда');
|
||
await page.click('#btnCloseShop');
|
||
|
||
// (f) миграция: старый сейв без dayTime/lastSaveMs
|
||
await page.evaluate(() => {
|
||
localStorage.setItem('fishing-save-v1', JSON.stringify({
|
||
coins: 77, rod: 0, lure: 0, rods: [0], lures: [0], muted: false,
|
||
caught: {}, casts: 1, earned: 10
|
||
}));
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m3m = await page.evaluate(() => {
|
||
const ls=JSON.parse(localStorage.getItem('fishing-save-v1'));
|
||
return { dayTime:dayTime, dayPhase:dayPhase, ms:typeof save.lastSaveMs,
|
||
lsMs:typeof ls.lastSaveMs, lsDay:ls.dayTime };
|
||
});
|
||
ok('M3 миграция: старый сейв → dayTime≈0 (morning), lastSaveMs установлен (память и localStorage)',
|
||
m3m.dayTime<0.01 && m3m.dayPhase==='morning' && m3m.ms==='number'
|
||
&& m3m.lsMs==='number' && Math.abs(m3m.lsDay)<0.01,
|
||
JSON.stringify(m3m));
|
||
|
||
// --- 9k. M4: босс-рыба (Осётр-босс) — 3 фазы, награда ×5, достижение, карточка ---
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
// форсируем босса и стурджа; квесты/стрик не должны искажать проверки
|
||
await page.evaluate(() => {
|
||
forceBoss = true; SHINY_CHANCE = 0;
|
||
window.__m4Pick = pickSpecies;
|
||
pickSpecies = () => SPECIES.find(s => s.id === 'sturge');
|
||
save.quests = { date: todayStr(), list: [] };
|
||
save.streak = { last: todayStr(), days: 1, shieldWeek: null };
|
||
save.bossBeaten = false; save.bossCount = 0; save.ach = {};
|
||
});
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
const m4a = await page.evaluate(() => ({
|
||
stateBoss, bossPhase, boss: reel.boss, spId: reel.sp.id, spBoss: reel.sp.boss, d0: reel.d0,
|
||
}));
|
||
ok('M4 босс: forceBoss + стурдж → старт босс-боя (stateBoss=true, bossPhase=1, reel.boss, sp.id=sturge)',
|
||
m4a.stateBoss === true && m4a.bossPhase === 1 && m4a.boss === true && m4a.spId === 'sturge' && m4a.spBoss === true,
|
||
JSON.stringify(m4a));
|
||
|
||
// бой: 3 фазы — d=0 → «обрыв» SNAP (без save.snaps++) → авто-REEL в следующую фазу → d=0 → … → фаза 3 → улов
|
||
const m4snapsBefore = await page.evaluate(() => save.snaps);
|
||
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'SNAP', 3000);
|
||
const m4p1 = await page.evaluate(() => {
|
||
const r = { snaps: save.snaps, boss: stateBoss, shake: shake > 0 };
|
||
reel.holding = false; // в обрыве не втягиваем — d фиксируется на старте фазы
|
||
return r;
|
||
});
|
||
await page.mouse.up();
|
||
await waitState(page, 'REEL', 3000);
|
||
const m4p2 = await page.evaluate(() => ({ ph: bossPhase, boss: stateBoss, d: reel.d, d0: reel.d0 }));
|
||
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'SNAP', 3000);
|
||
const m4p2snap = await page.evaluate(() => { reel.holding = false; return save.snaps; });
|
||
await page.mouse.up();
|
||
await waitState(page, 'REEL', 3000);
|
||
const m4p3 = await page.evaluate(() => ({ ph: bossPhase, d: reel.d, d0: reel.d0 }));
|
||
await page.evaluate(() => { window.__m4W = reel.w; reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||
await page.mouse.down();
|
||
await waitState(page, 'CARD', 5000);
|
||
await page.mouse.up();
|
||
const m4p4 = await page.evaluate(() => ({ st: state, boss: stateBoss, ph: bossPhase, snaps: save.snaps }));
|
||
ok('M4 босс-бой: 3 фазы (SNAP-обрыв без save.snaps++ → d≈0.6·d0 → d≈1.0·d0 → CARD, stateBoss=false)',
|
||
m4p1.snaps === m4snapsBefore && m4p1.boss === true && m4p1.shake === true
|
||
&& m4p2.ph === 2 && m4p2.boss === true && m4p2.d >= m4p2.d0 * 0.55 && m4p2.d <= m4p2.d0 * 0.65
|
||
&& m4p2snap === m4snapsBefore
|
||
&& m4p3.ph === 3 && m4p3.d >= m4p3.d0 * 0.95 && m4p3.d <= m4p3.d0 * 1.05
|
||
&& m4p4.st === 'CARD' && m4p4.boss === false && m4p4.ph === 0 && m4p4.snaps === m4snapsBefore,
|
||
JSON.stringify({ m4p1, m4p2, m4p3, m4p4 }));
|
||
|
||
// награда: цена ×5 (±10% — есть разброс ±5% в catchFish), bossBeaten/bossCount
|
||
const m4c = await page.evaluate(() => ({
|
||
val: parseInt(document.getElementById('cardVal').textContent.replace(/[^\d]/g, ''), 10),
|
||
w: window.__m4W,
|
||
price: SPECIES.find(s => s.id === 'sturge').price,
|
||
bossBeaten: save.bossBeaten, bossCount: save.bossCount,
|
||
}));
|
||
const m4base = m4c.w * m4c.price * 5;
|
||
ok('M4 босс-награда: монеты = вес×цена×5 (±10%), bossBeaten=true, bossCount=1',
|
||
m4c.val >= Math.max(1, Math.round(m4base * 0.9)) && m4c.val <= Math.round(m4base * 1.1)
|
||
&& m4c.bossBeaten === true && m4c.bossCount === 1,
|
||
JSON.stringify(m4c));
|
||
ok('M4 достижение: boss_first выдан после победы над боссом',
|
||
await page.evaluate(() => save.ach.boss_first === true));
|
||
const m4ui = await page.evaluate(() => ({
|
||
name: document.getElementById('cardName').textContent,
|
||
badgeHidden: document.getElementById('cardBoss').classList.contains('hidden'),
|
||
badgeText: document.getElementById('cardBoss').textContent,
|
||
bossCard: document.getElementById('card').classList.contains('boss-card'),
|
||
}));
|
||
ok('M4 карточка босса: имя «Осётр-БОСС», бейдж cardBoss виден, класс boss-card',
|
||
m4ui.name === 'Осётр-БОСС' && m4ui.badgeHidden === false && m4ui.badgeText.includes('БОСС') && m4ui.bossCard === true,
|
||
JSON.stringify(m4ui));
|
||
await page.screenshot({ path: 'shot-boss.png' });
|
||
await page.click('#btnCardOk');
|
||
// сброс тест-форсов
|
||
await page.evaluate(() => {
|
||
forceBoss = false; SHINY_CHANCE = 0.03;
|
||
pickSpecies = window.__m4Pick;
|
||
});
|
||
|
||
// --- 9l. Регрессии багфиксов спринта 3 ---
|
||
// (1) квест «Сделай N забросов»: questHit('casts') вызывался без condFn и падал
|
||
// TypeError на каждом забросе — квест не рос, а persist()/checkAchievements()
|
||
// в doCast() пропускались (счётчик забросов терялся при перезагрузке)
|
||
await page.evaluate(() => {
|
||
save.quests = { date: todayStr(), list: [{ tpl: 'casts', param: 0, goal: 1, prog: 0, done: false, reward: 60 }] };
|
||
save.streak = { last: todayStr(), days: 1, shieldWeek: null }; // стрик «сегодня» — не искажает монеты
|
||
save.coins = 100;
|
||
state = 'IDLE'; reel = null; stateBoss = false; bossPhase = 0; biteFish = null;
|
||
});
|
||
const castsQBefore = await page.evaluate(() => ({ casts: save.casts }));
|
||
const peBeforeCastsQ = pageErrors.length;
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
const castsQ = await page.evaluate(() => ({
|
||
prog: save.quests.list[0].prog,
|
||
done: save.quests.list[0].done,
|
||
coins: save.coins,
|
||
casts: save.casts,
|
||
lsCasts: JSON.parse(localStorage.getItem('fishing-save-v1')).casts,
|
||
hud: document.getElementById('coins').textContent,
|
||
}));
|
||
ok('багфикс квест «забросы»: квест растёт и выполняется на забросе (+60 🪙), без TypeError, заброс записан в сейв',
|
||
castsQ.prog === 1 && castsQ.done === true && castsQ.coins === 160
|
||
&& castsQ.casts === castsQBefore.casts + 1 && castsQ.lsCasts === castsQ.casts
|
||
&& pageErrors.length === peBeforeCastsQ,
|
||
JSON.stringify({ castsQ, pe: pageErrors.length - peBeforeCastsQ }));
|
||
// (2) награда квеста на забросе — вне catchFish: HUD монет должен обновиться сразу
|
||
ok('багфикс HUD квеста: счётчик монет обновлён сразу после выполнения квеста на забросе',
|
||
castsQ.hud === '🪙 ' + castsQ.coins, castsQ.hud);
|
||
|
||
// (2b) near-miss: утешение +10% цены тоже начисляется вне catchFish — HUD актуален
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
reel.w = 5; // фиксируем вес — утешение детерминированно и больше нуля
|
||
reel.phase = 'pull'; reel.phaseT = 9999;
|
||
reel.d = reel.d0 * 1.38;
|
||
});
|
||
await waitState(page, 'WAIT', 15000);
|
||
const nmHud = await page.evaluate(() => ({
|
||
coins: save.coins,
|
||
hud: document.getElementById('coins').textContent,
|
||
}));
|
||
ok('багфикс HUD near-miss: после «Сбежала! +N 🪙» счётчик монет актуален',
|
||
nmHud.hud === '🪙 ' + nmHud.coins, JSON.stringify(nmHud));
|
||
|
||
// (3) босс-бой: фазовый обрыв должен сбрасывать накопленную нагрузку —
|
||
// иначе «обрыв фазы без штрафа» с tension≈0.9·line тут же рвал шнур по-настоящему
|
||
await page.evaluate(() => {
|
||
forceBoss = true; SHINY_CHANCE = 0;
|
||
window.__pick2 = pickSpecies; pickSpecies = () => SPECIES.find(s => s.id === 'sturge');
|
||
state = 'IDLE'; reel = null; stateBoss = false; bossPhase = 0; biteFish = null;
|
||
save.quests = { date: todayStr(), list: [] };
|
||
});
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
await page.evaluate(() => {
|
||
reel.d = 2; reel.phase = 'calm'; reel.phaseT = 9999;
|
||
reel.tension = reel.line * 0.9; // рыба тянула почти до предела
|
||
});
|
||
await page.mouse.down();
|
||
await waitState(page, 'SNAP', 5000);
|
||
// палец НЕ отпускаем: после авто-возврата в REEL нагрузка должна стартовать с нуля
|
||
const bossPh = await page.evaluate(() => new Promise(res => {
|
||
const iv = setInterval(() => {
|
||
if (state === 'REEL') { clearInterval(iv); res({ st: state, ph: bossPhase, boss: stateBoss, tension: reel.tension, line: reel.line }); }
|
||
else if (state === 'SNAP' && !stateBoss) { clearInterval(iv); res({ st: 'REAL SNAP' }); }
|
||
}, 10);
|
||
}));
|
||
await page.mouse.up();
|
||
ok('багфикс босс-обрыв фазы: после авто-возврата в REEL нагрузка сброшена (tension≈0), бой продолжается',
|
||
bossPh.st === 'REEL' && bossPh.boss === true && bossPh.ph === 2 && bossPh.tension < bossPh.line * 0.1,
|
||
JSON.stringify(bossPh));
|
||
await page.evaluate(() => {
|
||
forceBoss = false; SHINY_CHANCE = 0.03; pickSpecies = window.__pick2;
|
||
state = 'IDLE'; reel = null; stateBoss = false; bossPhase = 0; biteFish = null;
|
||
});
|
||
|
||
// --- 9f. escape: рыба ускользает если долго не тянешь ---
|
||
// полный цикл: IDLE → CHARGE → WAIT → BITE → REEL
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down();
|
||
await sleep(500);
|
||
await page.mouse.up(); // бросок
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; }); // ускоряем клёв
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up(); // подсечка
|
||
await waitState(page, 'REEL');
|
||
// ставим рыбу в покой, чтобы escape не мешал, и фиксируем aggr
|
||
// (aggr обязателен: при ag≥0.65 бар заполняется за <2 с и рыба уходит до замера — T5)
|
||
await page.evaluate(() => { reel.phase = 'calm'; reel.phaseT = 9999; reel.holding = false; reel.aggr = 0.3; reel.escape = 0; });
|
||
await sleep(100);
|
||
const escBefore = await page.evaluate(() => reel.escape);
|
||
await sleep(2000); // 2 с без удержания — escape растёт
|
||
const escAfter = await page.evaluate(() => ({ escape: reel.escape, state }));
|
||
ok('escape: растёт когда палец отпущен',
|
||
escAfter.escape > escBefore && escAfter.state === 'REEL',
|
||
JSON.stringify({ before: escBefore, after: escAfter.escape }));
|
||
|
||
// теперь жмём палец — escape должен уменьшаться
|
||
await page.mouse.down();
|
||
await sleep(800);
|
||
const escHeld = await page.evaluate(() => reel.escape);
|
||
await page.mouse.up();
|
||
ok('escape: уменьшается когда палец нажат',
|
||
escHeld < escAfter.escape,
|
||
JSON.stringify({ afterHold: escHeld, was: escAfter.escape }));
|
||
|
||
// рыба сбегает при escape >= 1
|
||
await page.evaluate(() => { reel.escape = 0.95; reel.holding = false; reel.phase = 'calm'; reel.phaseT = 9999; });
|
||
await sleep(600);
|
||
const escGone = await page.evaluate(() => ({ escape: reel.escape, state, toast: document.getElementById('toast').textContent }));
|
||
ok('escape: рыба сбегает при escape >= 1 (state=WAIT, тост «Сбежала!»)',
|
||
escGone.state === 'WAIT' && escGone.toast.includes('Сбежала!'),
|
||
JSON.stringify(escGone));
|
||
|
||
// сброс для следующих тестов
|
||
await page.evaluate(() => {
|
||
state = 'IDLE'; reel = null; biteFish = null;
|
||
});
|
||
|
||
// --- 10. Спринт 4: физика (stamina, drag, пустые клёвы) ---
|
||
// Выводим рыбу на крючок и держим один бой для тестов (1)-(4),
|
||
// между тестами вручную возвращаем reel в безопасное состояние (d посередине,
|
||
// tension=0, line велик — чтобы бой не закончился ловлей/обрывом в окне замера).
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
await page.mouse.down(); await page.mouse.up();
|
||
await waitState(page, 'REEL');
|
||
|
||
// (1) stamina: reel.st есть и убывает в бою
|
||
const st0 = await page.evaluate(() => {
|
||
reel.d0 = 200; reel.d = 100; reel.tension = 0; reel.line = 500;
|
||
reel.phase = 'calm'; reel.phaseT = 9999; reel.holding = false;
|
||
return reel.st;
|
||
});
|
||
ok('stamina: reel.st инициализируется (>0)', st0 > 0, 'st0=' + st0.toFixed(3));
|
||
await page.mouse.down(); // holding=true — удерживаем
|
||
await sleep(2000);
|
||
const st1 = await page.evaluate(() => reel.st);
|
||
ok('stamina: reel.st убывает в бою (st<st0 после 2 с удержания)', st1 < st0,
|
||
'st0=' + st0.toFixed(3) + ' st1=' + st1.toFixed(3));
|
||
|
||
// (2) stamina: усталая рыба слабее — fishForce глобальна, tired лачится при st<=0.12
|
||
const f = await page.evaluate(() => {
|
||
const r = { st: 1, tired: false, ov: 1 };
|
||
const F1 = fishForce(r, 0.5, true);
|
||
r.st = 0.05;
|
||
const F2 = fishForce(r, 0.5, true);
|
||
return { F1, F2 };
|
||
});
|
||
ok('stamina: слабая/уставшая рыба тянет слабее (F(st=0.05) < 0.6·F(st=1))',
|
||
f.F2 < f.F1 * 0.6, 'F1=' + f.F1.toFixed(1) + ' F2=' + f.F2.toFixed(1));
|
||
await page.evaluate(() => {
|
||
reel.d0 = 200; reel.d = 100; reel.tension = 0; reel.line = 500;
|
||
reel.phase = 'calm'; reel.phaseT = 9999; reel.st = 0.1; reel.tired = false;
|
||
});
|
||
await sleep(200); // кадры updateReel зафиксируют tired при st<=0.12
|
||
const tiredFlag = await page.evaluate(() => reel.tired);
|
||
ok('stamina: при st=0.1 рыба устала (reel.tired===true)', tiredFlag === true,
|
||
'tired=' + tiredFlag);
|
||
|
||
// (3) drag: рывок сильнее drag — леска скользит (d уходит, tension за чертой)
|
||
await page.evaluate(() => {
|
||
reel.w = 12; reel.ov = 4; reel.aggr = 1; reel.st = 1; reel.tired = false;
|
||
reel.phase = 'pull'; reel.phaseT = 9999; reel.tension = 0; reel.line = 500;
|
||
reel.d0 = 100; reel.d = 100;
|
||
});
|
||
await sleep(1000); // палец всё ещё нажат (holding=true)
|
||
const slip = await page.evaluate(() => ({ d: reel.d, d0: reel.d0, tension: reel.tension, drag: rod().drag }));
|
||
ok('drag: рывок сильнее drag — леска скользит (d растёт, tension>drag)',
|
||
slip.d > slip.d0 && slip.tension > slip.drag,
|
||
JSON.stringify(slip));
|
||
|
||
// (4) drag: рывок слабее drag — спокойно втягиваем (d падает, tension под чертой)
|
||
await page.evaluate(() => {
|
||
reel.w = 0.3; reel.ov = 0.1; reel.aggr = 0.2; reel.st = 0.9; reel.tired = false;
|
||
reel.phase = 'pull'; reel.phaseT = 9999; reel.tension = 0; reel.line = 500;
|
||
reel.d0 = 100; reel.d = 100;
|
||
});
|
||
await sleep(1000);
|
||
const reelIn = await page.evaluate(() => ({ d: reel.d, d0: reel.d0, tension: reel.tension, drag: rod().drag }));
|
||
ok('drag: рывок слабее drag — втягивание (d падает, tension<drag)',
|
||
reelIn.d < reelIn.d0 && reelIn.tension < reelIn.drag,
|
||
JSON.stringify(reelIn));
|
||
await page.mouse.up(); // отпустили палец
|
||
await page.evaluate(() => { state = 'IDLE'; reel = null; biteFish = null; forceBite = false; });
|
||
|
||
// (5) пустой клёв: без forceBite и с Math.random=0.99+ клёва нет
|
||
await page.evaluate(() => { setDayTime(0.3); state = 'IDLE'; reel = null; biteFish = null; forceBite = false; });
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => {
|
||
window.__mr = Math.random;
|
||
Math.random = () => 0.999; // выше любого biteChance (макс ~0.9 днём)
|
||
biteTimer = 0.05;
|
||
forceBite = false;
|
||
});
|
||
await sleep(700);
|
||
const empty = await page.evaluate(() => {
|
||
Math.random = window.__mr;
|
||
return { st: state, bf: !!biteFish };
|
||
});
|
||
ok('пустой клёв: без forceBite и Math.random=0.99+ — клёва нет (WAIT, нет biteFish)',
|
||
empty.st === 'WAIT' && !empty.bf, JSON.stringify(empty));
|
||
|
||
// --- 10b. M5: апгрейды снастей (lv 1..4), фонарик, гейтинг лавки ---
|
||
// (1) миграция: старый сейв без rodLv/lureLv, фонарик ещё приманка (lures:[0,4], lure:4)
|
||
await page.evaluate(() => {
|
||
localStorage.setItem('fishing-save-v1', JSON.stringify({
|
||
coins: 77, rod: 0, lure: 4, rods: [0], lures: [0, 4], muted: false,
|
||
caught: {}, casts: 1, earned: 10
|
||
}));
|
||
});
|
||
await page.reload({ waitUntil: 'load' });
|
||
await sleep(500);
|
||
const m5m = await page.evaluate(() => ({
|
||
rodLv: save.rodLv, lureLv: save.lureLv,
|
||
lures: save.lures, lure: save.lure,
|
||
rods: save.rods, rod: save.rod
|
||
}));
|
||
ok('M5 миграция: старый сейв (нет rodLv/lureLv, lures:[0,4], lure:4) → rodLv={0:1}, lureLv={0:1}, lures без 4, lure=0',
|
||
m5m.rodLv[0]===1 && Object.keys(m5m.rodLv).length===1
|
||
&& m5m.lureLv[0]===1 && Object.keys(m5m.lureLv).length===1
|
||
&& m5m.lures.join(',')==='0' && m5m.lure===0
|
||
&& m5m.rods.join(',')==='0' && m5m.rod===0,
|
||
JSON.stringify(m5m));
|
||
|
||
// (2) данные: фонарика как приманки id=4 нет, поле nightOnly не используется
|
||
const m5t = await page.evaluate(() => ({
|
||
n: LURES.length,
|
||
ids: LURES.map(l => l.id).join(','),
|
||
noNight: LURES.every(l => !('nightOnly' in l))
|
||
}));
|
||
ok('M5 данные: LURES.length===4 (id 0..3, фонарика id=4 нет) и нет поля nightOnly',
|
||
m5t.n===4 && m5t.ids==='0,1,2,3' && m5t.noNight, JSON.stringify(m5t));
|
||
|
||
// (3) эффективные статы удочки 0 по уровням: точные (база × множители реализации)
|
||
const m5r = await page.evaluate(() => {
|
||
save.rod = 0;
|
||
const b = RODS[0], out = {};
|
||
for (const lv of [1, 2, 3, 4]) {
|
||
save.rodLv[0] = lv;
|
||
const r = rod();
|
||
out[lv] = { line: r.line, drag: r.drag, reel: r.reel, size: r.size, light: r.light };
|
||
}
|
||
save.rodLv[0] = 1;
|
||
return {
|
||
got: out,
|
||
exp: {
|
||
1: { line: b.line, drag: b.drag, reel: b.reel, size: b.size, light: false },
|
||
2: { line: b.line * 1.15, drag: b.drag, reel: b.reel, size: b.size, light: true },
|
||
3: { line: b.line * 1.15, drag: b.drag * 1.15, reel: b.reel * 1.10, size: b.size, light: true },
|
||
4: { line: b.line * 1.15 * 1.15, drag: b.drag * 1.15, reel: b.reel * 1.10, size: b.size + 0.10, light: true }
|
||
}
|
||
};
|
||
});
|
||
ok('M5 статы удочки 0: Lv1=база (line 100, drag 34, reel 1.0, size 0), Lv2 line≈115 (×1.15), Lv3 drag≈39.1 (×1.15) reel=1.1 (×1.10), Lv4 line≈132.25 (×1.15²) size=0.1',
|
||
[1, 2, 3, 4].every(lv => {
|
||
const g = m5r.got[lv], e = m5r.exp[lv];
|
||
return g.line === e.line && g.drag === e.drag && g.reel === e.reel && g.size === e.size && !!g.light === e.light;
|
||
}),
|
||
JSON.stringify(m5r.got));
|
||
|
||
// (4) эффективные статы приманки 0 по уровням
|
||
const m5l = await page.evaluate(() => {
|
||
save.lure = 0;
|
||
const b = LURES[0], out = {};
|
||
for (const lv of [1, 2, 3, 4]) {
|
||
save.lureLv[0] = lv;
|
||
const l = lure();
|
||
out[lv] = { bite: l.bite, rare: l.rare, size: l.size };
|
||
}
|
||
save.lureLv[0] = 1;
|
||
return {
|
||
got: out,
|
||
exp: {
|
||
1: { bite: b.bite, rare: b.rare, size: b.size },
|
||
2: { bite: b.bite * 1.15, rare: b.rare, size: b.size },
|
||
3: { bite: b.bite * 1.15, rare: b.rare + 1.0, size: b.size },
|
||
4: { bite: b.bite * 1.15, rare: b.rare + 1.0, size: b.size + 0.15 }
|
||
}
|
||
};
|
||
});
|
||
ok('M5 статы приманки 0: Lv1=база (bite 1.0, rare 0, size 0), Lv2 bite=1.15 (×1.15), Lv3 rare=1.0 (+1.0), Lv4 size=0.15 (+0.15)',
|
||
[1, 2, 3, 4].every(lv => {
|
||
const g = m5l.got[lv], e = m5l.exp[lv];
|
||
return g.bite === e.bite && g.rare === e.rare && g.size === e.size;
|
||
}),
|
||
JSON.stringify(m5l.got));
|
||
|
||
// (5) фонарик = апгрейд 1 удилища: hasLight
|
||
const m5h = await page.evaluate(() => {
|
||
save.rodLv[save.rod] = 1; const a = hasLight();
|
||
save.rodLv[save.rod] = 2; const b = hasLight();
|
||
save.rodLv[save.rod] = 4; const c = hasLight();
|
||
save.rodLv[save.rod] = 1;
|
||
return { a, b, c };
|
||
});
|
||
ok('M5 hasLight(): false на 1 ур., true на 2 ур. и 4 ур.',
|
||
m5h.a === false && m5h.b === true && m5h.c === true, JSON.stringify(m5h));
|
||
|
||
// (6) лавка: гейтинг — свежий сейв, rod 1 и lure 1 заблокированы
|
||
await page.click('#btnPlay');
|
||
await sleep(150);
|
||
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;
|
||
});
|
||
await page.click('#btnShop');
|
||
await sleep(150);
|
||
await page.click('#tabTackle');
|
||
await sleep(150);
|
||
const m5g1 = await page.evaluate(() => {
|
||
const items = Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||
const byName = n => items.find(el => el.querySelector('.itemname').textContent.includes(n));
|
||
const info = el => ({
|
||
locked: el.classList.contains('locked'),
|
||
buy: el.querySelector('[data-act="buy"]') !== null,
|
||
lock: el.querySelector('button[disabled]') !== null,
|
||
desc: el.querySelector('.itemdesc').textContent
|
||
});
|
||
return { r1: info(byName('Стеклопластик')), l1: info(byName('Червь')) };
|
||
});
|
||
ok('M5 гейтинг лавки: свежий сейв (1 ур.) — строки rod 1 и lure 1: класс locked, 🔒, нет buy, «Откроется: … до 4 ур.»',
|
||
m5g1.r1.locked && !m5g1.r1.buy && m5g1.r1.lock
|
||
&& m5g1.r1.desc.includes('Откроется') && m5g1.r1.desc.includes('до 4 ур.')
|
||
&& m5g1.l1.locked && !m5g1.l1.buy && m5g1.l1.lock && m5g1.l1.desc.includes('Откроется'),
|
||
JSON.stringify(m5g1));
|
||
|
||
// (7) лавка: предыдущая снасть на 4 ур. — buy появляется, locked снимается
|
||
const m5g2 = await page.evaluate(() => {
|
||
save.rodLv[0] = 4; save.lureLv[0] = 4;
|
||
renderShop();
|
||
const items = Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||
const byName = n => items.find(el => el.querySelector('.itemname').textContent.includes(n));
|
||
const info = el => {
|
||
const b = el.querySelector('[data-act="buy"]');
|
||
return { locked: el.classList.contains('locked'), buyTxt: b ? b.textContent : null };
|
||
};
|
||
return { r1: info(byName('Стеклопластик')), l1: info(byName('Червь')) };
|
||
});
|
||
ok('M5 гейтинг лавки: после rodLv[0]=4 / lureLv[0]=4 — locked снят, кнопки buy «250 🪙» / «150 🪙»',
|
||
!m5g2.r1.locked && m5g2.r1.buyTxt === '250 🪙' && !m5g2.l1.locked && m5g2.l1.buyTxt === '150 🪙',
|
||
JSON.stringify(m5g2));
|
||
|
||
// (8) двойная защита: buy-ветка shopClick при закрытом гейтинге не покупает
|
||
const m5d = await page.evaluate(() => {
|
||
save.rodLv[0] = 1; save.coins = 1000;
|
||
const before = { rods: save.rods.join(','), coins: save.coins };
|
||
const el = document.createElement('button');
|
||
el.dataset.act = 'buy'; el.dataset.type = 'rod'; el.dataset.i = '1';
|
||
document.body.appendChild(el);
|
||
let err = null;
|
||
try { shopClick({ target: el }); } catch (e) { err = String(e); }
|
||
document.body.removeChild(el);
|
||
return { err, before,
|
||
after: { rods: save.rods.join(','), coins: save.coins },
|
||
toast: document.getElementById('toast').textContent };
|
||
});
|
||
ok('M5 двойная защита: shopClick buy (rod 1) при rodLv[0]<4 — rods/coins не изменились, тост «Сначала улучши…»',
|
||
m5d.err === null && m5d.before.rods === m5d.after.rods && m5d.before.coins === m5d.after.coins
|
||
&& m5d.toast.includes('Сначала улучши'),
|
||
JSON.stringify(m5d));
|
||
|
||
// (9) покупка апгрейда: 1000 🪙 → 600, 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;
|
||
renderShop();
|
||
});
|
||
await page.click('#shopBody [data-act="upgrade"][data-type="rod"][data-i="0"]');
|
||
const m5u = await page.evaluate(() => ({
|
||
coins: save.coins, lv: save.rodLv[0], light: hasLight(),
|
||
toast: document.getElementById('toast').textContent
|
||
}));
|
||
ok('M5 покупка апгрейда: rod 0 1→2 ур. за 400 🪙 (1000→600), hasLight()=true, тост «Улучшено … → 2 ур»',
|
||
m5u.coins === 600 && m5u.lv === 2 && m5u.light === true
|
||
&& m5u.toast.includes('Улучшено') && m5u.toast.includes('2 ур'),
|
||
JSON.stringify(m5u));
|
||
|
||
// (10) на макс. уровне (4) кнопки upgrade в строке нет
|
||
const m5x = await page.evaluate(() => {
|
||
save.rodLv[0] = 4; renderShop();
|
||
const r0 = document.querySelectorAll('#shopBody .item')[0];
|
||
return { hasUpg: r0.querySelector('[data-act="upgrade"]') !== null, txt: r0.textContent };
|
||
});
|
||
ok('M5 макс. уровень: на 4 ур. у rod 0 кнопки upgrade нет (пипсы ●●●●, «4 ур.»)',
|
||
!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.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 🪙 на апгрейд за 400 — coins=100, уровень не изменился, тост «Не хватает монет»',
|
||
m5p.coins === 100 && m5p.lv === 1 && m5p.toast.includes('Не хватает монет'),
|
||
JSON.stringify(m5p));
|
||
await page.click('#btnCloseShop');
|
||
|
||
// (12) ночь без фонарика (rodLv[save.rod]=1): клёв не стартует, тост (паттерн §9j(c))
|
||
await page.evaluate(() => {
|
||
setDayTime(0.8);
|
||
save.lures = [0]; save.lure = 0;
|
||
save.rodLv[save.rod] = 1;
|
||
save.quests = { date: todayStr(), list: [] }; // квестный тост не затирает проверочный
|
||
state = 'IDLE'; reel = null; biteFish = null; forceBite = false;
|
||
});
|
||
await page.mouse.move(195, 600);
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await sleep(600);
|
||
const m5n = await page.evaluate(() => ({
|
||
st: state, bf: !!biteFish, bt: biteTimer,
|
||
toast: document.getElementById('toast').textContent
|
||
}));
|
||
ok('M5 ночь без фонарика (rodLv=1): клёв не стартует (WAIT, нет biteFish, biteTimer<0), тост «Ночью нужен фонарик»',
|
||
m5n.st === 'WAIT' && !m5n.bf && m5n.bt < 0 && m5n.toast.includes('Ночью нужен фонарик'),
|
||
JSON.stringify(m5n));
|
||
|
||
// (13) ночь с фонариком (rodLv[save.rod]=2): клёв работает (паттерн §9j(d))
|
||
await page.click('#btnRecall');
|
||
await waitState(page, 'IDLE');
|
||
await page.mouse.move(195, 600);
|
||
await page.evaluate(() => { save.rodLv[save.rod] = 2; });
|
||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||
await waitState(page, 'WAIT');
|
||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||
await waitState(page, 'BITE', 4000);
|
||
ok('M5 ночь с фонариком (rodLv=2): клёв работает (BITE)', true);
|
||
|
||
// (14) покупка разблокированной снасти стартует с уровня 1
|
||
await page.evaluate(() => {
|
||
forceBite = false; state = 'IDLE'; reel = null; biteFish = null;
|
||
setDayTime(0.3);
|
||
save.rods = [0]; save.lures = [0]; save.rod = 0; save.lure = 0;
|
||
save.rodLv = { 0: 4 }; save.lureLv = { 0: 1 }; save.coins = 1000;
|
||
});
|
||
await page.click('#btnShop');
|
||
await sleep(150);
|
||
await page.click('#tabTackle');
|
||
await sleep(150);
|
||
await page.click('#shopBody [data-act="buy"][data-type="rod"][data-i="1"]');
|
||
const m5b = await page.evaluate(() => ({
|
||
rods: save.rods.join(','), lv: save.rodLv[1],
|
||
rod: save.rod, coins: save.coins
|
||
}));
|
||
ok('M5 покупка разблокированной снасти: rod 1 за 250 🪙 — rods=[0,1], rodLv[1]=1, снасть экипирована, coins=750',
|
||
m5b.rods === '0,1' && m5b.lv === 1 && m5b.rod === 1 && m5b.coins === 750,
|
||
JSON.stringify(m5b));
|
||
await page.click('#btnCloseShop');
|
||
|
||
// --- 11. Ошибки консоли ---
|
||
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
|
||
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));
|
||
|
||
await browser.close();
|
||
|
||
const failed = results.filter(r => !r.pass);
|
||
console.log('\n===== ИТОГ: ' + (results.length - failed.length) + '/' + results.length + ' =====');
|
||
process.exit(failed.length ? 1 : 0);
|
||
})().catch(e => { console.error('TEST CRASH:', e.message); process.exit(2); });
|