import { describe, it, expect } from 'vitest';
import { inCircle, inCircleW, inCone, inConeW, angleBetween, nearest } from '../shapes';
import { worldToScreen } from '../iso';
describe('inCircle', () => {
it('точка внутри и на границе', () => {
const c = { x: 0, y: 0 };
expect(inCircle(c, 10, { x: 3, y: 4 })).toBe(true);
expect(inCircle(c, 5, { x: 3, y: 4 })).toBe(true); // ровно 5
expect(inCircle(c, 5, { x: 3, y: 5 })).toBe(false);
expect(inCircle(c, 0, { x: 0, y: 0 })).toBe(true);
});
it('точка вне центра', () => {
expect(inCircle({ x: 100, y: 50 }, 10, { x: 0, y: 0 })).toBe(false);
// расстояние sqrt(50² + 30²) ≈ 58.3
expect(inCircle({ x: 100, y: 50 }, 60, { x: 50, y: 20 })).toBe(true);
});
});
describe('inCone', () => {
const from = { x: 0, y: 0 };
const dir = { x: 1, y: 0 }; // смотрит вправо
const halfAngle = Math.PI / 4; // 45°
it('внутри конуса', () => {
expect(inCone(from, dir, 50, halfAngle, { x: 30, y: 10 })).toBe(true);
expect(inCone(from, dir, 50, halfAngle, { x: 30, y: -10 })).toBe(true);
});
it('за пределами дальности', () => {
expect(inCone(from, dir, 50, halfAngle, { x: 60, y: 0 })).toBe(false);
});
it('за границей угла («слепая зона» за спиной)', () => {
expect(inCone(from, dir, 50, halfAngle, { x: 30, y: 40 })).toBe(false);
expect(inCone(from, dir, 50, halfAngle, { x: -30, y: 0 })).toBe(false);
});
it('вблизи границы угла и радиуса', () => {
// чуть внутри границы угла 45° (tan 45°=1)
expect(inCone(from, dir, 50, halfAngle, { x: 10, y: 9.9 })).toBe(true);
// чуть снаружи
expect(inCone(from, dir, 50, halfAngle, { x: 10, y: 10.1 })).toBe(false);
// расстояние ровно 50 — граница радиуса включена
expect(inCone(from, dir, 50, halfAngle, { x: 50, y: 0 })).toBe(true);
expect(inCone(from, dir, 50, halfAngle, { x: 50.1, y: 0 })).toBe(false);
});
it('нулевое направление — только дальность', () => {
expect(inCone(from, { x: 0, y: 0 }, 50, halfAngle, { x: 0, y: -40 })).toBe(true);
expect(inCone(from, { x: 0, y: 0 }, 50, halfAngle, { x: 60, y: 0 })).toBe(false);
});
});
describe('angleBetween', () => {
it('известные углы', () => {
expect(angleBetween({ x: 1, y: 0 }, { x: 0, y: 1 })).toBeCloseTo(Math.PI / 2);
expect(angleBetween({ x: 1, y: 0 }, { x: -1, y: 0 })).toBeCloseTo(Math.PI);
expect(angleBetween({ x: 2, y: 0 }, { x: 5, y: 0 })).toBeCloseTo(0);
});
it('нулевой вектор даёт 0 (без NaN)', () => {
expect(angleBetween({ x: 0, y: 0 }, { x: 1, y: 0 })).toBe(0);
expect(angleBetween({ x: 1, y: 0 }, { x: 0, y: 0 })).toBe(0);
});
});
describe('nearest', () => {
it('выбирает ближайшую', () => {
const list = [
{ x: 50, y: 0 },
{ x: 10, y: 0 },
{ x: 30, y: 0 }
];
expect(nearest(list, { x: 0, y: 0 })).toEqual({ x: 10, y: 0 });
});
it('учитывает maxRange', () => {
const list = [{ x: 100, y: 0 }];
expect(nearest(list, { x: 0, y: 0 }, 50)).toBeNull();
expect(nearest(list, { x: 0, y: 0 }, 100)).toEqual({ x: 100, y: 0 });
});
it('пустой список — null', () => {
expect(nearest([], { x: 0, y: 0 })).toBeNull();
});
});
describe('W-варианты (мировые юниты)', () => {
const iso = { tileW: 32, tileH: 16, originX: 0, originY: 0 };
it('inCircleW эквивалентен проекции в экранные px', () => {
const center = { x: 3, y: 1 };
for (let wx = 0; wx <= 6; wx += 0.5) {
for (let wy = 0; wy <= 2; wy += 0.5) {
const point = { x: wx, y: wy };
expect(inCircleW(center, 1.5, point, iso)).toBe(
inCircle(worldToScreen(center.x, center.y, iso), 48, worldToScreen(point.x, point.y, iso))
);
}
}
});
it('inConeW эквивалентен проекции в экранные px', () => {
const from = { x: 3, y: 1 };
const dir = { x: 1, y: -1 }; // «вправо» в мировых юнитах
for (let wx = 0; wx <= 6; wx += 0.5) {
for (let wy = 0; wy <= 2; wy += 0.5) {
const point = { x: wx, y: wy };
expect(inConeW(from, dir, 2, Math.PI / 3, point, iso)).toBe(
inCone(
worldToScreen(from.x, from.y, iso),
worldToScreen(dir.x, dir.y, iso),
64,
Math.PI / 3,
worldToScreen(point.x, point.y, iso)
)
);
}
}
});
});