/
docNemo
/
hex-map-editor
Обзор
Документация
Войти
/
docNemo
/
hex-map-editor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
5
CI/CD
Аналитика
Безопасность
main
tests/layout.test.ts
207 строк
9 KB
docNemo
feat(art): особые зоны накладываются на биом, а не подменяют его
03 авг 2026, 15:35
03 авг 2026, 15:35
d94bebf
Код
Авторство
О чём код?
import { describe, expect, it } from 'vitest'; import { HEX_DIRECTIONS, hexNeighbor, type HexCoord } from '../src/core/hex'; import { foundryCenter, foundryOriginOffset, gridSizeFromHexHeight, HEX_ASPECT_RATIO, HexLayout, hexHeightFromGridSize, SPRITE_OVERSCALE, } from '../src/core/layout'; /** Единица раскладки flat-top — высота гекса. */ const HEIGHT = 400; describe('геометрия flat-top гекса', () => { it('соотношение сторон равно 2/√3', () => { expect(HEX_ASPECT_RATIO).toBeCloseTo(1.154700538, 9); }); it('ширина спрайта следует из высоты по геометрии шестиугольника', () => { // Именно это соотношение зафиксировано в assets/ART-SPEC.md: 462 × 400. const layout = new HexLayout(HEIGHT); expect(layout.hexWidth).toBeCloseTo(461.88, 2); expect(Math.round(layout.hexWidth)).toBe(462); }); it('нулевая и отрицательная высота отвергаются', () => { expect(() => new HexLayout(0)).toThrow(); expect(() => new HexLayout(-10)).toThrow(); }); }); describe('расположение центров', () => { const layout = new HexLayout(HEIGHT); it('гекс {0,0} находится в начале координат', () => { expect(layout.center({ q: 0, r: 0 })).toEqual({ x: 0, y: 0 }); }); it('шаг внутри столбца равен высоте гекса', () => { const a = layout.center({ q: 0, r: 0 }); const b = layout.center({ q: 0, r: 1 }); expect(b.y - a.y).toBeCloseTo(HEIGHT, 6); expect(b.x - a.x).toBeCloseTo(0, 6); }); it('шаг между столбцами равен трём четвертям ширины', () => { const a = layout.center({ q: 0, r: 0 }); const b = layout.center({ q: 1, r: 0 }); expect(b.x - a.x).toBeCloseTo(0.75 * layout.hexWidth, 6); }); it('соседний столбец сдвинут на половину высоты', () => { const a = layout.center({ q: 0, r: 0 }); const b = layout.center({ q: 1, r: 0 }); expect(b.y - a.y).toBeCloseTo(HEIGHT / 2, 6); }); it('все шесть соседей равноудалены от центра', () => { const origin: HexCoord = { q: 3, r: -2 }; const center = layout.center(origin); for (const direction of HEX_DIRECTIONS) { const neighbor = layout.center(hexNeighbor(origin, direction)); expect(Math.hypot(neighbor.x - center.x, neighbor.y - center.y)).toBeCloseTo(HEIGHT, 6); } }); }); describe('обратное преобразование', () => { const layout = new HexLayout(HEIGHT); it('центр гекса отображается обратно в тот же гекс', () => { for (let r = -6; r <= 6; r += 1) { for (let q = -6; q <= 6; q += 1) { expect(layout.hexAt(layout.center({ q, r }))).toEqual({ q, r }); } } }); it('точка около центра принадлежит тому же гексу', () => { const coord: HexCoord = { q: 2, r: 3 }; const center = layout.center(coord); const offsets = [ { x: 10, y: 10 }, { x: -30, y: 25 }, { x: 40, y: -50 }, ]; for (const offset of offsets) { expect(layout.hexAt({ x: center.x + offset.x, y: center.y + offset.y })).toEqual(coord); } }); }); describe('размеры спрайта и карты', () => { const layout = new HexLayout(HEIGHT); it('спрайт увеличен ровно на коэффициент перекрытия швов', () => { const { width, height } = layout.spriteSize(); expect(height).toBeCloseTo(HEIGHT * SPRITE_OVERSCALE, 6); expect(width).toBeCloseTo(layout.hexWidth * SPRITE_OVERSCALE, 6); expect(SPRITE_OVERSCALE).toBeGreaterThan(1); }); it('габариты карты вмещают все гексы вместе со сдвигом столбцов', () => { const { width, height } = layout.mapSize(40, 40); expect(height).toBeCloseTo(40 * HEIGHT + HEIGHT / 2, 6); expect(width).toBeCloseTo(39 * 0.75 * layout.hexWidth + layout.hexWidth, 6); }); it('карта в один столбец не получает лишнего сдвига', () => { expect(layout.mapSize(1, 5).height).toBeCloseTo(5 * HEIGHT, 6); }); }); describe('соответствие размеру сетки Foundry', () => { it('перевод в обе стороны взаимно обратен', () => { for (const size of [50, 100, 140, 400]) { expect(gridSizeFromHexHeight(hexHeightFromGridSize(size))).toBe(size); } }); it('grid.size соответствует высоте гекса', () => { // Подтверждено замером на Foundry 14.365, сетка HEXODDQ: при grid.size = 100 // grid.sizeY = 100 (высота), grid.sizeX = 115.4701 (ширина). const layout = new HexLayout(hexHeightFromGridSize(100)); expect(layout.hexHeight).toBe(100); expect(layout.hexWidth).toBeCloseTo(115.4701, 4); }); it('шаги между гексами совпадают с замеренными в Foundry', () => { const layout = new HexLayout(100); const origin = layout.center({ q: 0, r: 0 }); // Замер: переход на следующий ряд (0, +100). const nextRow = layout.center({ q: 0, r: 1 }); expect(nextRow.x - origin.x).toBeCloseTo(0, 4); expect(nextRow.y - origin.y).toBeCloseTo(100, 4); // Замер: шаг по столбцу (+86.6026, +50) — в выводе замера значение округлено // до четырёх знаков, точная величина равна 100 × √3/2. const nextColumn = layout.center({ q: 1, r: 0 }); expect(nextColumn.x - origin.x).toBeCloseTo((100 * Math.sqrt(3)) / 2, 9); expect(nextColumn.x - origin.x).toBeCloseTo(86.6026, 3); expect(nextColumn.y - origin.y).toBeCloseTo(50, 4); }); it('смещение начала координат ставит центр первого гекса на полширины', () => { // Foundry помещает центр гекса {0,0} в (ширина/2, 0): его левая вершина // касается левого края сцены. const layout = new HexLayout(100); const offset = foundryOriginOffset(layout.hexHeight); expect(offset).toEqual({ x: layout.hexWidth / 2, y: 0 }); const centre = foundryCenter(layout, { q: 0, r: 0 }); expect(centre.x).toBeCloseTo(57.735, 4); expect(centre.y).toBeCloseTo(0, 6); }); it('смещение постоянно и не накапливается', () => { const layout = new HexLayout(100); for (const coord of [ { q: 0, r: 0 }, { q: 2, r: 2 }, { q: 20, r: 39 }, ]) { const plain = layout.center(coord); const foundry = foundryCenter(layout, coord); expect(foundry.x - plain.x).toBeCloseTo(layout.hexWidth / 2, 9); expect(foundry.y - plain.y).toBeCloseTo(0, 9); } }); it('смещение чувствительно к тому, что в него передали', () => { // Ловушка, на которую наступил слой отрисовки: он передавал в смещение // ширину вместо высоты. Значения отличаются ровно в 2/√3 раза, и вся карта // уезжает вправо — но по отдельности каждое выглядит правдоподобно. const layout = new HexLayout(100); const correct = foundryOriginOffset(layout.hexHeight); const wrong = foundryOriginOffset(layout.hexWidth); expect(correct.x).toBeCloseTo(57.735, 3); expect(wrong.x / correct.x).toBeCloseTo(HEX_ASPECT_RATIO, 9); }); it('совпадает с центрами, замеренными в живом Foundry', () => { // Замер HEXODDQ, grid.size = 100. Смещённые координаты {i: ряд, j: столбец}. const layout = new HexLayout(100); const at = (col: number, row: number) => foundryCenter(layout, { q: col, r: row - (col - (col & 1)) / 2 }); expect(at(0, 0).x).toBeCloseTo(57.735, 3); expect(at(0, 0).y).toBeCloseTo(0, 3); const c01 = at(1, 0); expect(c01.x - at(0, 0).x).toBeCloseTo(86.6026, 3); expect(c01.y - at(0, 0).y).toBeCloseTo(50, 3); const c11 = at(1, 1); expect(c11.x - at(0, 0).x).toBeCloseTo(86.6026, 3); expect(c11.y - at(0, 0).y).toBeCloseTo(150, 3); const c02 = at(2, 0); expect(c02.x - at(0, 0).x).toBeCloseTo(173.2051, 3); expect(c02.y - at(0, 0).y).toBeCloseTo(0, 3); }); });