/**
* Сценарий collision — коллизии и карта коллизий агенту.
* 1) слой collision в снапшоте: стены/вода/пропы, тайл героя свободен.
* 2) scene:walkable: трава проходима, вода и дерево — нет.
* 3) scene:raycast: дерево перекрывает луч, вода прозрачна (плевок летит над прудом).
* 4) scene:walk в непроходимый — false; approach подводит к соседу.
* 5) расталкивание тел: враг выталкивает себя из героя.
* Запуск: node tools/agent.mjs run tools/checks/collision.mjs
*/
import { withChecks } from '../lib.mjs';
export default async function ({ pretty }) {
return withChecks(
'collision',
async (t) => {
const { c } = t;
await c.run('снапшот: слой collision валиден', async () => {
await t.boot();
const s = await t.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) => t.ctx.agent.command('scene:walkable', { x, y });
const s = await t.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) => t.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('scene:walk в непроходимый — false, approach — к соседу', async () => {
await t.sleepEnemies();
// Ближе к дереву: маршрут от (2,3) не задевает спящих ползунов у тропы
// (сам (2,2) — тайл перехода на пруды, туда телепортировать нельзя).
await t.ctx.agent.command('scene:teleport', { x: 2, y: 3 });
await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 60 });
// Путь в дерево (0,4) не строится: scene:walk — false.
const walked = await t.ctx.agent.command('scene:walk', { x: 0, y: 4 });
c.expect(walked === false, 'scene:walk провёл героя в дерево', walked);
// approach подводит к проходимому соседу дерева.
const dest = await t.ctx.agent.approach(0, 4);
c.expect(dest !== null, 'approach к дереву не нашёл соседа');
const s = await t.ctx.agent.snapshot();
c.expect(
s.hero.tile.x === dest.x && s.hero.tile.y === dest.y,
'герой не на тайле прибытия approach',
{ dest, after: s.hero.tile }
);
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 t.sleepEnemies();
// Ползун спит в (10.5,7.5); телепортируемся внутрь его тела.
await t.ctx.agent.command('scene:teleport', { x: 10, y: 7 });
await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 60 });
const s = await t.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;
});
},
{ pretty }
);
}