import { describe, it, expect } from 'vitest';
import type { Vec2 } from '../Vec2';
import {
worldToScreen,
screenToWorld,
unitsToPx,
pxToUnits,
worldLen,
worldNorm,
worldDist,
moveTowardsW,
tileToWorld,
worldToTile,
worldRectToScreen,
DEFAULT_ISO
} from '../iso';
describe('проекция мир↔экран', () => {
it('совпадает с iso-формулой', () => {
// Центр тайла (2, 3) на экране: (2.5-3.5)*16, (2.5+3.5)*8
const s = worldToScreen(2.5, 3.5);
expect(s.x).toBeCloseTo(-16, 10);
expect(s.y).toBeCloseTo(48, 10);
});
it('round-trip worldToScreen ∘ screenToWorld', () => {
for (let wx = -5; wx <= 5; wx += 0.7) {
for (let wy = -5; wy <= 5; wy += 0.7) {
const s = worldToScreen(wx, wy);
const back = screenToWorld(s.x, s.y);
expect(back.x).toBeCloseTo(wx, 10);
expect(back.y).toBeCloseTo(wy, 10);
}
}
});
it('учитывает origin', () => {
const iso = { ...DEFAULT_ISO, originX: 100, originY: 50 };
const s = worldToScreen(1, 0, iso);
expect(s.x).toBeCloseTo(100 + 16, 10);
expect(s.y).toBeCloseTo(50 + 8, 10);
});
});
describe('скаляры: юниты↔пиксели', () => {
it('линейка проекции — tileW px на юнит', () => {
expect(unitsToPx(1)).toBe(32);
expect(unitsToPx(1.5)).toBe(48);
expect(pxToUnits(32)).toBe(1);
expect(pxToUnits(48)).toBe(1.5);
expect(pxToUnits(unitsToPx(2.75))).toBeCloseTo(2.75, 10);
});
});
describe('метрика проекции', () => {
it('длины характерных смещений', () => {
// Экранные оси — мировые диагонали: «вправо» = (1,-1) -> 1 юнит.
expect(worldLen(1, -1)).toBeCloseTo(1, 10);
expect(worldLen(-1, 1)).toBeCloseTo(1, 10);
// Диагональ сетки (1,1) — вертикальный шаг: на экране 8+8=16 px -> 0.5 юнита.
expect(worldLen(1, 1)).toBeCloseTo(0.5, 10);
// По оси сетки: соседние тайлы — sqrt(16² + 8²)/32 ≈ 0.56 юнита.
expect(worldLen(1, 0)).toBeCloseTo(Math.hypot(16, 8) / 32, 10);
});
it('worldDist совпадает с worldLen', () => {
expect(worldDist({ x: 0, y: 0 }, { x: 3, y: -3 })).toBeCloseTo(3, 10);
expect(worldDist({ x: 1, y: 2 }, { x: 1, y: 2 })).toBeCloseTo(0, 10);
});
it('worldNorm даёт постоянную экранную скорость', () => {
// Нормализованный вектор в юнитах при скорости s юнит/с даёт ровно 32·s px/с.
const s = 1.75;
const dirs: Vec2[] = [
{ x: 1, y: 0 },
{ x: 0, y: 1 },
{ x: 1, y: 1 },
{ x: -1, y: 1 },
{ x: 2, y: -1 }
];
for (const d of dirs) {
const n = worldNorm(d.x, d.y);
const step = { x: n.x * s, y: n.y * s };
const px = worldToScreen(step.x, step.y);
expect(Math.hypot(px.x, px.y)).toBeCloseTo(32 * s, 6);
}
});
it('worldNorm нулевого вектора', () => {
expect(worldNorm(0, 0)).toEqual({ x: 0, y: 0 });
});
});
describe('moveTowardsW', () => {
const a = { x: 0, y: 0 };
const b = { x: 3, y: -3 }; // 3 юнита
it('не проскакивает цель', () => {
expect(moveTowardsW(a, b, 10)).toEqual(b);
});
it('постоянная экранная скорость на любом направлении', () => {
const step = moveTowardsW(a, b, 1.75);
const px = worldToScreen(step.x, step.y);
expect(Math.hypot(px.x, px.y)).toBeCloseTo(32 * 1.75, 6);
});
it('последовательность шагов достигает цели', () => {
let p = a;
for (let i = 0; i < 100 && p !== b; i++) {
const next = moveTowardsW(p, b, 1.75);
if (next === p) break;
p = next;
}
expect(p).toEqual(b);
});
});
describe('тайлы↔мир', () => {
it('tileToWorld — центр тайла', () => {
expect(tileToWorld(2, 3)).toEqual({ x: 2.5, y: 3.5 });
});
it('worldToTile — floor внутри карты', () => {
expect(worldToTile(2.5, 3.5, 10, 10)).toEqual({ x: 2, y: 3 });
expect(worldToTile(0, 0, 10, 10)).toEqual({ x: 0, y: 0 });
});
it('worldToTile вне карты -> null', () => {
expect(worldToTile(-0.1, 5, 10, 10)).toBeNull();
expect(worldToTile(5, 10, 10, 10)).toBeNull();
});
});
describe('worldRectToScreen', () => {
it('карта 28x28 — границы от западного угла', () => {
const r = worldRectToScreen(0, 0, 28, 28);
expect(r).toEqual({ x: -448, y: 0, width: 896, height: 448 });
});
it('произвольный прямоугольник', () => {
// Углы (2,0) и (2,1): x от (2-1)*16 до 2*16.
const r = worldRectToScreen(2, 0, 1, 1);
expect(r.x).toBeCloseTo((2 - 1) * 16, 10);
expect(r.width).toBeCloseTo(2 * 16, 10);
});
});