/**
* Сценарий 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;
}