Newer
Older
rpg / apps / game / tools / checks / collision.mjs
/**
 * Сценарий collision — коллизии и карта коллизий агенту.
 * 1) слой collision в снапшоте: стены/вода/пропы, тайл героя свободен.
 * 2) scene:walkable: трава проходима, вода и дерево — нет.
 * 3) scene:raycast: дерево перекрывает луч, вода прозрачна (плевок летит над прудом).
 * 4) walkTo в непроходимый тайл: герой остаётся в проходимом.
 * 5) расталкивание тел: враг выталкивает себя из героя.
 * Запуск: node tools/agent.mjs run tools/checks/collision.mjs
 */
import { startDevServer, openGame, Checks } from '../lib.mjs';

export default async function ({ pretty }) {
    const c = new Checks('collision');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('снапшот: слой collision валиден', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            const s = await ctx.agent.snapshot();
            const col = s.collision;
            c.expect(col && col.width > 0 && col.height > 0, 'нет карты коллизий', col);
            c.expect(
                col.blocked.length === col.width * col.height,
                'blocked не накрывает карту',
                { len: col.blocked.length, w: col.width, h: col.height }
            );
            c.expect(
                col.blocked.every((v) => v === 0 || v === 1),
                'blocked не бинарный',
                col.blocked.filter((v) => v !== 0 && v !== 1)
            );
            c.expect(
                col.props.every((p) => p.w >= 1 && p.h >= 1),
                'проп без footprint',
                col.props
            );
            const idx = s.hero.tile.y * col.width + s.hero.tile.x;
            c.expect(col.blocked[idx] === 0, 'герой заспавнился в блоке', s.hero.tile);
            return null;
        });
        await c.run('scene:walkable: вода и дерево непроходимы', async () => {
            const walk = (x, y) => ctx.agent.command('scene:walkable', { x, y });
            const s = await ctx.agent.snapshot();
            const heroWalk = await walk(s.hero.tile.x, s.hero.tile.y);
            c.expect(heroWalk === true, 'тайл героя непроходим', s.hero.tile);
            c.expect((await walk(19, 8)) === false, 'вода проходима?');
            c.expect((await walk(0, 4)) === false, 'дерево проходимо?');
            return null;
        });
        await c.run('scene:raycast: дерево перекрывает, вода прозрачна', async () => {
            const ray = (from, to) => ctx.agent.command('scene:raycast', { from, to });
            // Вертикальный луч (3,8)->(3,10) задевает дерево (3,9).
            c.expect((await ray({ x: 3, y: 8 }, { x: 3, y: 10 })) === false, 'дерево не перекрыло луч');
            // Берег-берег через пруд: вода прозрачна, луч чист.
            c.expect((await ray({ x: 18, y: 8 }, { x: 24, y: 8 })) === true, 'вода перекрыла луч');
            return null;
        });
        await c.run('клик в непроходимый тайл: герой остаётся в проходимом', async () => {
            await ctx.agent.command('scene:sleepAll');
            // Ближе к дереву: маршрут от (2,3) не задевает спящих ползунов у тропы
            // (сам (2,2) — тайл перехода на пруды, туда телепортировать нельзя).
            await ctx.agent.command('scene:teleport', { x: 2, y: 3 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 60 });
            // Клик по дереву (0,4): A* ведёт к проходимому соседу.
            await ctx.agent.tapTile(0, 4);
            const w = await ctx.agent.waitFor('s.hero && !s.hero.moving', { timeoutTicks: 1200 });
            c.expect(w.ok, 'герой не остановился после клика по дереву');
            const s = await ctx.agent.snapshot();
            const col = s.collision;
            const idx = s.hero.tile.y * col.width + s.hero.tile.x;
            c.expect(col.blocked[idx] === 0, 'герой стоит в блоке', s.hero.tile);
            return null;
        });
        await c.run('расталкивание: враг выталкивает себя из героя', async () => {
            await ctx.agent.command('scene:sleepAll');
            // Ползун спит в (10.5,7.5); телепортируемся внутрь его тела.
            await ctx.agent.command('scene:teleport', { x: 10, y: 7 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 60 });
            const s = await ctx.agent.snapshot();
            const en = (s.enemies ?? []).find(
                (e) => !e.dead && Math.hypot(e.pos.x - 10.5, e.pos.y - 7.5) < 1.2
            );
            c.expect(!!en, 'ползун у тропы не найден', s.enemies);
            const gap = Math.hypot(en.pos.x - s.hero.pos.x, en.pos.y - s.hero.pos.y);
            c.expect(gap >= 0.4, 'враг остался внутри тела героя', { gap, en: en.pos, hero: s.hero.pos });
            return null;
        });
    } finally {
        await ctx?.browser?.close();
        server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}