import { describe, expect, it } from 'vitest';
import { VoxelWorld } from '../../world/world';
import { makeBody, stepBody, collides, GRAVITY } from '../body';
import type { Body } from '../body';
/** Мир сцены теста: земля/платформы/стены/потолок. Высота мира — 16 (аргумент VoxelWorld). */
function testWorld(): VoxelWorld {
const w = new VoxelWorld(16);
// земля y=0..1 для x=0..9; уступ ниже (y=0) для x≥10
for (let x = 0; x < 10; x++)
for (let z = 0; z < 8; z++) {
w.set(x, 0, z, 1);
w.set(x, 1, z, 1);
}
for (let x = 10; x < 16; x++)
for (let z = 0; z < 8; z++) w.set(x, 0, z, 1);
// стена-колонна x=6 (толщина 1), y=2..4 — торчит над землёй
for (let y = 2; y <= 4; y++)
for (let z = 0; z < 8; z++) w.set(6, y, z, 1);
// потолок-плита y=7 над x=0..3 (не задевает x=5, где падает тестовое тело)
for (let x = 0; x <= 3; x++)
for (let z = 0; z < 8; z++) w.set(x, 7, z, 1);
// вторая стена z=0 (x=4..8, y=2..4) — вместе со стеной x=6 образует угол
for (let x = 4; x <= 8; x++)
for (let y = 2; y <= 4; y++) w.set(x, y, 0, 1);
return w;
}
/** Прогнать шагов физики, пока не выполнится предикат (с лимитом). */
function runUntil(body: Body, world: VoxelWorld, pred: () => boolean, dt = 1 / 60, limit = 600): void {
for (let i = 0; i < limit && !pred(); i++) stepBody(world, body, dt);
}
describe('collides (AABB против вокселей)', () => {
it('в свободном пространстве — нет, с пересечением вокселя — да', () => {
const w = testWorld();
expect(collides(w, [5, 3, 5], [1, 2, 1])).toBe(false); // над землёй, вне стены
expect(collides(w, [5, 1, 5], [1, 2, 1])).toBe(true); // ноги в земле
});
it('касание грани снаружи — не столкновение', () => {
const w = testWorld();
expect(collides(w, [5, 2, 5], [1, 2, 1])).toBe(false); // ноги ровно на земле (y=2)
});
});
describe('stepBody (падение и опора)', () => {
it('тело падает и встаёт на землю ровно на грань', () => {
const w = testWorld();
const body = makeBody([5, 6, 5], [1, 2, 1]);
runUntil(body, w, () => body.grounded);
expect(body.pos[1]).toBeCloseTo(2);
expect(body.vel[1]).toBe(0);
});
it('стоит на месте: позиция не плывёт, grounded держится', () => {
const w = testWorld();
const body = makeBody([5, 2, 5], [1, 2, 1]);
body.grounded = true;
for (let i = 0; i < 120; i++) stepBody(w, body, 1 / 60);
expect(body.pos[1]).toBeCloseTo(2);
expect(body.grounded).toBe(true);
expect(body.pos[0]).toBeCloseTo(5);
});
it('сход с уступа: падает с верхней площадки на нижнюю', () => {
const w = testWorld();
const body = makeBody([9, 2, 5], [1, 2, 1]);
runUntil(body, w, () => body.grounded); // сначала встать на верхнюю площадку
body.vel[0] = 3; // идём на восток, за x=10 земля ниже
runUntil(body, w, () => !body.grounded);
expect(body.pos[0]).toBeGreaterThanOrEqual(9.5);
runUntil(body, w, () => body.grounded && body.pos[0] > 10.5);
expect(body.pos[1]).toBeCloseTo(1); // нижний уровень: грань y=1
});
});
describe('stepBody (стены и зажимы)', () => {
it('стена толщиной 1 останавливает, скорость обнуляется', () => {
const w = testWorld();
const body = makeBody([5, 2, 5], [1, 2, 1]); // запад от стены x=6
body.vel[0] = 4;
runUntil(body, w, () => body.vel[0] === 0);
expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3); // упёрся в западную грань стены
expect(body.vel[0]).toBe(0);
expect(body.grounded).toBe(true);
});
it('быстрое движение не туннелирует сквозь тонкую стену', () => {
const w = testWorld();
const body = makeBody([5, 2, 5], [1, 2, 1]);
body.vel[0] = 50; // 50 вокс/с при dt=0.1 — 5 вокселей за шаг
stepBody(w, body, 0.1);
expect(body.pos[0]).toBeLessThan(6);
expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3);
});
it('угол: две стены по осям зажимают независимо', () => {
const w = testWorld();
const body = makeBody([5, 2, 2], [1, 2, 1]); // у угла стен x=6 и z=0
body.vel[0] = 4;
body.vel[2] = -4;
runUntil(body, w, () => body.vel[0] === 0 && body.vel[2] === 0);
expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3); // зажат западной гранью стены x=6
expect(body.pos[2]).toBeCloseTo(1 + 0.5 + 1e-4, 3); // ...и северной гранью стены z=0
expect(body.pos[1]).toBeCloseTo(2); // вертикаль не пострадала
});
it('потолок гасит подъём', () => {
const w = testWorld();
const body = makeBody([3, 2, 5], [1, 2, 1]); // под плитой y=7
body.vel[1] = 20;
runUntil(body, w, () => body.vel[1] === 0 && body.pos[1] > 3);
expect(body.pos[1]).toBeCloseTo(7 - 2 - 1e-4, 2); // голова упёрлась в грань y=7
expect(body.grounded).toBe(false);
});
});
describe('гравитация (число)', () => {
it('полунеявный Эйлер: за шаг dt скорость −G·dt, позиция −G·dt²', () => {
const w = testWorld();
const body = makeBody([8, 20, 5], [1, 2, 1]); // вдали от стен, высоко
stepBody(w, body, 0.5);
expect(body.vel[1]).toBeCloseTo(-GRAVITY * 0.5, 6);
expect(body.pos[1]).toBeCloseTo(20 - GRAVITY * 0.5 * 0.5, 6);
});
});