Newer
Older
rpg / apps / game / tools / checks / lighting.mjs
/**
 * Сценарий lighting — освещение сцены через мост: ambient областей, окна
 * Звенца, очаг дома Ирвина (флаг квеста), лампа героя, мерцание.
 * 1) Луга: светлый дневной ambient, источников нет (и лампы — улица светлая);
 * 2) Звенец: серые сумерки, три окна домов, башня молчит (флага нет);
 * 3) Дом Ирвина: тёмный интерьер, hearth_low (не hearth_high), лампа героя;
 *    мерцание очага — интенсивность гуляет по серии снапшотов;
 * 4) телепорт внутри дома — лампа следует за героем (дистанция лампа↔очаг
 *    меняется; разность экранных координат камерой не компенсируется);
 * 5) quest_bells_done: hearth_high вместо hearth_low и строго ярче;
 * 6) инварианты чисты.
 * Запуск: node tools/agent.mjs run tools/checks/lighting.mjs
 */
import { startDevServer, openGame, Checks } from '../lib.mjs';

/** Ambient областей (те же константы, что в data/locations.ts). */
const AMB = { meadows: 0xd8dade, zvenets: 0xdad6d0, house_elder: 0x54586a };

/** Экранное смещение источника b относительно a (px). */
function lightVec(s, a, b) {
    const x = s.lighting.sources.find((l) => l.id === a);
    const y = s.lighting.sources.find((l) => l.id === b);
    return x && y ? { x: y.x - x.x, y: y.y - x.y } : null;
}

export default async function ({ pretty }) {
    const c = new Checks('lighting');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('луга: дневной ambient, источников нет', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            const s = await ctx.agent.snapshot();
            c.expect(s.lighting.ambient === AMB.meadows, 'ambient лугов не дневной', s.lighting);
            c.expect(s.lighting.sources.length === 0, 'на светлой улице не должно быть источников', s.lighting.sources);
            return null;
        });
        await c.run('Звенец: сумерки, три окна, башня молчит', async () => {
            await ctx.agent.walkTo(26, 14, { timeoutTicks: 3000 });
            const w = await ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 600 });
            c.expect(w.ok, 'не пришли в Звенец');
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            const s = await ctx.agent.snapshot();
            c.expect(s.lighting.ambient === AMB.zvenets, 'ambient Звенца не сумеречный', s.lighting);
            const windows = s.lighting.sources.filter((l) => l.id.startsWith('window_'));
            c.expect(windows.length === 3, 'окон домов должно быть три', s.lighting.sources);
            c.expect(!s.lighting.sources.some((l) => l.id === 'tower_resonance'), 'башня светит без звонка', s.lighting.sources);
            c.expect(!s.lighting.sources.some((l) => l.id === 'lamp'), 'на улице светлой деревни лампе героя не место', s.lighting.sources);
            return null;
        });
        await c.run('дом Ирвина: тёмный интерьер, hearth_low, лампа героя', async () => {
            await ctx.agent.tapTile(7, 6); // дверь дома — клик-переход
            const w = await ctx.agent.waitFor('s.area === "house_elder"', { timeoutTicks: 1200 });
            c.expect(w.ok, 'не вошли в дом Ирвина');
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            const s = await ctx.agent.snapshot();
            c.expect(s.lighting.ambient === AMB.house_elder, 'ambient интерьера не тёмный', s.lighting);
            c.expect(s.lighting.sources.some((l) => l.id === 'hearth_low'), 'нет тлеющего очага', s.lighting.sources);
            c.expect(!s.lighting.sources.some((l) => l.id === 'hearth_high'), 'очаг горит ровно до сдачи квеста', s.lighting.sources);
            c.expect(s.lighting.sources.some((l) => l.id === 'lamp'), 'в тёмном доме нет лампы героя', s.lighting.sources);
            // Мерцание: серия снапшотов — интенсивность очага гуляет (с допуском).
            const seen = new Set();
            for (let i = 0; i < 8; i++) {
                const t = await ctx.agent.snapshot();
                const h = t.lighting.sources.find((l) => l.id === 'hearth_low');
                if (h) seen.add(Math.round(h.intensity * 100));
                await ctx.agent.step(6);
            }
            c.expect(seen.size >= 3, `мерцание очага не видно (вариантов ${seen.size})`, [...seen]);
            return null;
        });
        await c.run('лампа героя: следует за телепортом', async () => {
            const v1 = lightVec(await ctx.agent.snapshot(), 'hearth_low', 'lamp');
            await ctx.agent.command('scene:teleport', { x: 2, y: 2 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 300 });
            const v2 = lightVec(await ctx.agent.snapshot(), 'hearth_low', 'lamp');
            c.expect(v1 !== null && v2 !== null, 'нет лампы или очага в снапшоте', { v1, v2 });
            // Очаг статичен: v2 - v1 — чистое смещение лампы (камера двигает оба поровну).
            const shift = Math.hypot(v2.x - v1.x, v2.y - v1.y);
            c.expect(shift > 60, `лампа не переехала за героем (смещение ${shift.toFixed(0)} px)`, { v1, v2 });
            return null;
        });
        await c.run('сдача квеста: hearth_high вместо hearth_low и ярче', async () => {
            await ctx.agent.command('scene:setFlag', { flag: 'quest_bells_done' });
            const w = await ctx.agent.waitFor('s.lighting.sources.some((l) => l.id === "hearth_high")', {
                timeoutTicks: 300
            });
            c.expect(w.ok, 'после сдачи квеста очаг не разгорелся', w.snapshot.lighting.sources);
            const s = await ctx.agent.snapshot();
            c.expect(!s.lighting.sources.some((l) => l.id === 'hearth_low'), 'тлеющий очаг не погас', s.lighting.sources);
            const high = s.lighting.sources.find((l) => l.id === 'hearth_high');
            // Минимум hearth_high (1.05·0.8) выше максимума hearth_low (0.75).
            c.expect(high.intensity > 0.84 - 0.02, 'hearth_high не ярче тлеющего', high);
            return null;
        });
        await c.run('инварианты чисты при работающем свете', async () => {
            const inv = await ctx.agent.invariants();
            const bad = inv.filter((i) => i.severity === 'error');
            c.expect(bad.length === 0, 'ошибки инвариантов', bad);
            return null;
        });
    } finally {
        await ctx?.browser?.close();
        server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}