Newer
Older
rpg / v2 / apps / game / tools / checks / weather.mjs
/**
 * Гейт погоды v2 (трек «погода в движке»): база погоды по областям, ручной
 * режим (дождь/снег рисуются и падают, облачность темнит кадр), кадр без
 * осадков, сброс к погоде области, управление из дев-панели. Скриншоты:
 * /tmp/v2_weather_*.png.
 *
 * Запуск: node apps/game/tools/checks/weather.mjs [--port 5330]
 */
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { launchBrowser, startDevServer, openGame, parseArgs } from '@rpg/engine/tools/agent-lib.mjs';
import { decodePng } from '@rpg/engine/tools/png.mjs';

const args = parseArgs(process.argv.slice(2));
const port = Number(args.port ?? 5330);

const server = startDevServer({ port, cwd: fileURLToPath(new URL('../../', import.meta.url)) });
const report = { name: 'weather', ok: false, checks: [] };
const check = (title, ok, info = null) => {
    report.checks.push({ title, ok, info });
    console.error(`${ok ? '✓' : '✗'} ${title}${info ? ' — ' + JSON.stringify(info) : ''}`);
};

/** Средняя яркость кадра 0..255 (облачность темнит кадр). */
function brightness(rgba) {
    let sum = 0, n = 0;
    for (let i = 0; i < rgba.length; i += 4) {
        sum += rgba[i] * 0.3 + rgba[i + 1] * 0.6 + rgba[i + 2] * 0.1;
        n++;
    }
    return sum / n;
}

/** Число различающихся пикселей двух кадров (осадки движутся). */
function countDiff(a, b) {
    let n = 0;
    for (let i = 0; i < a.length; i += 4) {
        if (Math.abs(a[i] - b[i]) > 8 || Math.abs(a[i + 1] - b[i + 1]) > 8 || Math.abs(a[i + 2] - b[i + 2]) > 8) n++;
    }
    return n;
}

async function shot(client, path) {
    await client.screenshot(path);
    return decodePng(readFileSync(path)).rgba;
}

try {
    await server.waitReady;
    const browser = await launchBrowser();
    try {
        const client = await openGame(browser, { url: `http://localhost:${port}/` });
        await client.call('startGame', false); // страница стартует в меню (меню/автосейв)
        const errs = client.errors();
        check('нет pageerror при загрузке', errs.length === 0, errs.slice(0, 3));

        await client.call('setManual', true);
        await client.call('setClock', 720); // полдень — облачность мерить на ровном дне
        await client.call('stepTicks', 5);

        // ---- База погоды области ----
        let wi = await client.call('weatherInfo');
        check('луга: база — лёгкий ветер, облачность 0.15, без осадков',
            wi.windStrength === 0.15 && wi.cloud === 0.15 && wi.rain === 0 && wi.snow === 0 && wi.manual === null, wi);

        await client.call('gotoArea', 'old_siding');
        await client.call('stepTicks', 60); // фейд перехода 0.4с — дождаться конца
        wi = await client.call('weatherInfo');
        check('разъезд: база — продувает (ветер 0.6, дождь 0.15)',
            wi.windStrength === 0.6 && wi.cloud === 0.35 && wi.rain === 0.15, wi);

        // ---- Ручной ливень: рисуется и падает ----
        await client.call('setWeather', { rain: 1, snow: 0, cloud: 0, windStrength: 0 });
        await client.call('stepTicks', 3);
        wi = await client.call('weatherInfo');
        check('ручной ливень: 260 капель, снег 0, правка панели активна',
            wi.rainDrops === 260 && wi.snowFlakes === 0 && wi.manual !== null, wi);
        const rainA = await shot(client, '/tmp/v2_weather_rain.png');
        await client.call('stepTicks', 30); // стена дождя падает (22 юнит/с)
        const rainB = await shot(client, '/tmp/v2_weather_rain2.png');
        const rainDiff = countDiff(rainA, rainB);
        check('дождь падает: кадры различаются', rainDiff > 100, { rainDiff });

        // ---- Снег: медленный, рисуется и падает ----
        await client.call('setWeather', { rain: 0, snow: 1, cloud: 0, windStrength: 0 });
        await client.call('stepTicks', 3);
        wi = await client.call('weatherInfo');
        check('ручной снег: 200 снежинок, дождь 0', wi.snowFlakes === 200 && wi.rainDrops === 0, wi);
        const snowA = await shot(client, '/tmp/v2_weather_snow.png');
        await client.call('stepTicks', 40); // снег медленный — шаг подлиннее
        const snowB = await shot(client, '/tmp/v2_weather_snow2.png');
        const snowDiff = countDiff(snowA, snowB);
        check('снег падает: кадры различаются', snowDiff > 100, { snowDiff });

        // ---- Облачность темнит кадр (день) ----
        await client.call('gotoArea', 'meadows');
        await client.call('stepTicks', 60); // фейд перехода — дождаться конца
        await client.call('setWeather', { rain: 0, snow: 0, cloud: 0, windStrength: 0 });
        await client.call('stepTicks', 2);
        const clearBright = brightness(await shot(client, '/tmp/v2_weather_clear.png'));
        await client.call('setWeather', { cloud: 1 });
        await client.call('stepTicks', 3);
        const cloudBright = brightness(await shot(client, '/tmp/v2_weather_cloud.png'));
        check('облачность темнит кадр (полная — заметно темнее)',
            cloudBright < clearBright * 0.8, { clearBright, cloudBright });

        // ---- setAsh(false): гейт-режим глушит и осадки с облачностью ----
        await client.call('setWeather', null); // ручная правка переживает смену области — снять
        await client.call('gotoArea', 'rust_grove'); // база рощи — морось 0.3
        await client.call('stepTicks', 60); // фейд перехода — дождаться конца
        wi = await client.call('weatherInfo');
        check('роща: база — морось (rain 0.3, облачность 0.5)',
            wi.rain === 0.3 && wi.cloud === 0.5 && wi.manual === null, wi);
        await client.call('setAsh', false);
        wi = await client.call('weatherInfo');
        check('setAsh(false): осадки и облачность глушатся (кадр-равенства)',
            wi.rainDrops === 0 && wi.snowFlakes === 0 && wi.cloud === 0, wi);

        // ---- Сброс возвращает базу области ----
        await client.call('setWeather', { rain: 0.9, windStrength: 1 });
        await client.call('setWeather', null);
        wi = await client.call('weatherInfo');
        check('сброс: вернулась база рощи, правок панели нет',
            wi.rain === 0.3 && wi.cloud === 0.5 && wi.manual === null, wi);

        // ---- Дев-панель: слайдер дождя реально управляет погодой ----
        await client.call('setAsh', true); // вернуть атмосферу после гейт-режима
        await client.call('devOpen');
        const hasSliders = await client.evaluate(() =>
            ['dev-w-cloud', 'dev-w-rain', 'dev-w-snow', 'dev-w-wind', 'dev-w-dir']
                .every((id) => document.getElementById(id) !== null));
        check('дев-панель: секция погоды с 5 слайдерами', hasSliders === true);
        // слайдер дождя → input-событие → ручная правка активна
        await client.evaluate(() => {
            const el = document.getElementById('dev-w-rain');
            el.value = '1';
            el.dispatchEvent(new Event('input'));
        });
        wi = await client.call('weatherInfo');
        check('дев-панель: слайдер дождя включает ручной режим', wi.manual?.rain === 1, wi);
        await client.call('devSetWeather', null); // сброс + синхронизация слайдеров
        wi = await client.call('weatherInfo');
        check('дев-панель: сброс вернул базу области', wi.manual === null && wi.windStrength === 0.2, wi);
        await client.call('devClose');
    } finally {
        await browser.close();
    }
} catch (e) {
    console.error(e);
    report.fatal = String(e);
}

report.ok = report.checks.length > 0 && report.checks.every((c) => c.ok);
console.log(JSON.stringify(report));
process.exit(report.ok ? 0 : 1); // chromium-хэндлы держат event loop — выходим явно