124 lines
5.4 KiB
JavaScript
124 lines
5.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const ts = require('typescript');
|
|
|
|
const exported = {};
|
|
const source = fs.readFileSync(path.join(__dirname, '../assets/Script/LaserRules.ts'), 'utf8');
|
|
vm.runInNewContext(ts.transpileModule(source, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
|
|
}).outputText, { exports: exported });
|
|
const { nearestLaserHit } = exported;
|
|
const point = (x, y) => ({ x, y });
|
|
const rect = (minX, minY, maxX, maxY) => ({ type: 'rect', minX, minY, maxX, maxY });
|
|
const polygon = coordinates => ({ type: 'polygon', points: coordinates.map(([x, y]) => point(x, y)) });
|
|
const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`);
|
|
|
|
for (const [direction, start, end, expected] of [
|
|
['right', point(0, 5), point(10, 5), point(4, 5)],
|
|
['left', point(10, 5), point(0, 5), point(6, 5)],
|
|
['up', point(5, 0), point(5, 10), point(5, 4)],
|
|
['down', point(5, 10), point(5, 0), point(5, 6)],
|
|
]) {
|
|
test(`${direction} beam ends at the nearest rectangle surface`, () => {
|
|
const hit = nearestLaserHit(start, end, [rect(4, 4, 6, 6)]);
|
|
close(hit.ratio, 0.4);
|
|
close(hit.distance, 4);
|
|
close(hit.point.x, expected.x);
|
|
close(hit.point.y, expected.y);
|
|
});
|
|
}
|
|
|
|
test('nearest blocker wins regardless of collider ordering', () => {
|
|
const hit = nearestLaserHit(point(0, 0), point(10, 0), [
|
|
rect(8, -1, 9, 1),
|
|
{ type: 'circle', center: point(5, 0), radius: 1 },
|
|
polygon([[2, -1], [3, -1], [3, 1], [2, 1]]),
|
|
]);
|
|
assert.equal(hit.colliderIndex, 2);
|
|
close(hit.distance, 2);
|
|
});
|
|
|
|
test('fractional dragging positions unblock immediately, without grid snapping', () => {
|
|
const start = point(0, 0), end = point(10, 0);
|
|
const moving = rect(4.125, -0.5, 5.125, 0.5);
|
|
close(nearestLaserHit(start, end, [moving]).distance, 4.125);
|
|
moving.minY += 0.501;
|
|
moving.maxY += 0.501;
|
|
assert.equal(nearestLaserHit(start, end, [moving]), null);
|
|
moving.minY -= 0.002;
|
|
moving.maxY -= 0.002;
|
|
close(nearestLaserHit(start, end, [moving]).distance, 4.125);
|
|
});
|
|
|
|
test('a beam in the empty part of a concave shape is not blocked by its bounds', () => {
|
|
const lShape = polygon([[2, 0], [6, 0], [6, 1], [3, 1], [3, 4], [2, 4]]);
|
|
assert.equal(nearestLaserHit(point(4, 3), point(7, 3), [lShape]), null);
|
|
const hit = nearestLaserHit(point(7, 3), point(0, 3), [lShape]);
|
|
close(hit.point.x, 3);
|
|
close(hit.distance, 4);
|
|
lShape.points.reverse();
|
|
close(nearestLaserHit(point(7, 3), point(0, 3), [lShape]).distance, 4);
|
|
});
|
|
|
|
test('circle surface, tangent, and a clear line are distinguished', () => {
|
|
const circle = { type: 'circle', center: point(5, 0), radius: 2 };
|
|
close(nearestLaserHit(point(0, 0), point(10, 0), [circle]).distance, 3);
|
|
close(nearestLaserHit(point(0, 2), point(10, 2), [circle]).distance, 5);
|
|
assert.equal(nearestLaserHit(point(0, 2.001), point(10, 2.001), [circle]), null);
|
|
});
|
|
|
|
test('an emitter inside any solid has a zero-distance hit', () => {
|
|
const solids = [
|
|
rect(-1, -1, 1, 1),
|
|
{ type: 'circle', center: point(0, 0), radius: 1 },
|
|
polygon([[-1, -1], [1, -1], [1, 1], [-1, 1]]),
|
|
];
|
|
for (const solid of solids) {
|
|
const hit = nearestLaserHit(point(0, 0), point(10, 0), [solid]);
|
|
assert.equal(hit.ratio, 0);
|
|
assert.equal(hit.distance, 0);
|
|
}
|
|
});
|
|
|
|
test('touching a solid boundary blocks, including a collinear polygon edge', () => {
|
|
for (const solid of [rect(3, 0, 5, 2), polygon([[3, 0], [5, 0], [5, 2], [3, 2]])]) {
|
|
close(nearestLaserHit(point(0, 0), point(10, 0), [solid]).distance, 3);
|
|
close(nearestLaserHit(point(10, 0), point(0, 0), [solid]).distance, 5);
|
|
assert.equal(nearestLaserHit(point(3, 1), point(0, 1), [solid]).ratio, 0);
|
|
}
|
|
});
|
|
|
|
test('objects behind the emitter or beyond the receiver do not block', () => {
|
|
const solids = [
|
|
rect(-3, -1, -2, 1), rect(11, -1, 12, 1),
|
|
{ type: 'circle', center: point(13, 0), radius: 2 },
|
|
polygon([[11, -1], [12, -1], [12, 1], [11, 1]]),
|
|
];
|
|
assert.equal(nearestLaserHit(point(0, 0), point(10, 0), solids), null);
|
|
close(nearestLaserHit(point(0, 0), point(10, 0), [rect(10, -1, 11, 1)]).ratio, 1);
|
|
});
|
|
|
|
test('rotated polygon surfaces use their edges, not their axis-aligned bounds', () => {
|
|
const diamond = polygon([[5, 0], [7, 2], [5, 4], [3, 2]]);
|
|
close(nearestLaserHit(point(0, 1), point(10, 1), [diamond]).distance, 4);
|
|
const diagonal = nearestLaserHit(point(0, 0), point(10, 10), [rect(3, 3, 4, 4)]);
|
|
close(diagonal.ratio, 0.3);
|
|
close(diagonal.distance, Math.sqrt(18));
|
|
});
|
|
|
|
test('crossing beams are queried independently against the same solids', () => {
|
|
const blockers = [rect(7, 4, 8, 6)];
|
|
close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7);
|
|
assert.equal(nearestLaserHit(point(5, 0), point(5, 10), blockers), null);
|
|
close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7);
|
|
});
|
|
|
|
test('empty colliders and zero-length lines have defined results', () => {
|
|
assert.equal(nearestLaserHit(point(0, 0), point(10, 0), []), null);
|
|
assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(1, 1, 2, 2)]), null);
|
|
assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(-1, -1, 1, 1)]).distance, 0);
|
|
});
|