Files
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

203 lines
11 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { attachBlockLightSamplers, blockEmission, buildBlockLight } from "../block-light.js";
import { unitBox } from "../math.js";
const stone = { state: "minecraft:stone", render: [unitBox], light: 0, light_dampening: 15, light_occlusion: [] };
const torch = { state: "minecraft:torch", render: [], light: 14, light_dampening: 0, light_occlusion: [] };
const lamp = { ...stone, state: "minecraft:redstone_lamp[lit=true]", light: 15 };
const materials = () => new Map([
[1, stone], [2, torch], [3, lamp],
[4, { ...lamp, state: "minecraft:redstone_lamp[lit=false]", light: 0 }],
[5, { ...torch, state: "minecraft:soul_torch", light: 10 }],
[6, { ...torch, state: "minecraft:sea_lantern", light: 15 }],
[7, { ...stone, state: "minecraft:glass", transparent: true, opacity: 0.35, light_dampening: 0 }],
]);
const map = (...cells) => new Map(cells.map(([pos, id]) => [pos.join(","), id]));
const bounds = { min: [-16, -3, -3], max: [16, 5, 3] };
const sample = (field, pos) => field.sample(pos.map((value) => value + 0.5));
test("block light uses canonical source levels, one-cell falloff and finite reach", () => {
const field = buildBlockLight(map([[0, 0, 0], 2]), materials(), bounds);
assert.equal(field.sourceCount, 1);
for (let x = 0; x <= 15; x++) assert.equal(sample(field, [x, 0, 0]).blockLevel, Math.max(0, 14 - x));
assert.equal(sample(field, [3, 2, 1]).blockLevel, 8);
assert.equal(sample(field, [0, 0, 0]).skyLevel, 15);
});
test("closed solid walls occlude light without wraparound in flattened arrays", () => {
const blocks = map([[-2, 0, 0], 3]);
for (let y = -3; y <= 5; y++) for (let z = -3; z <= 3; z++) blocks.set(`0,${y},${z}`, 1);
const field = buildBlockLight(blocks, materials(), bounds);
assert.equal(sample(field, [-1, 0, 0]).blockLevel, 14);
assert.equal(sample(field, [0, 0, 0]).blockLevel, 0);
assert.equal(sample(field, [1, 0, 0]).blockLevel, 0);
assert.deepEqual(sample(field, [1, 0, 0]).block, [0, 0, 0]);
});
test("light travels around an open doorway with Manhattan path attenuation", () => {
const blocks = map([[-2, 0, 0], 3]);
for (let y = -3; y <= 5; y++) for (let z = -3; z <= 3; z++) if (y !== 0 || z !== 2) blocks.set(`0,${y},${z}`, 1);
const field = buildBlockLight(blocks, materials(), bounds);
assert.equal(sample(field, [1, 0, 0]).blockLevel, 8);
});
test("transparent glass transmits while metadata dampening controls attenuation", () => {
const blocks = map([[-2, 0, 0], 3]);
for (let y = -3; y <= 5; y++) for (let z = -3; z <= 3; z++) blocks.set(`0,${y},${z}`, 7);
const field = buildBlockLight(blocks, materials(), bounds);
assert.equal(sample(field, [1, 0, 0]).blockLevel, 12);
const mats = materials();
mats.set(7, { ...mats.get(7), light_dampening: 3 });
assert.equal(sample(buildBlockLight(blocks, mats, bounds), [1, 0, 0]).blockLevel, 10);
});
test("authoritative unlit state emits zero and recomputation removes stale light", () => {
const blocks = map([[0, 0, 0], 3]);
assert.equal(sample(buildBlockLight(blocks, materials(), bounds), [1, 0, 0]).blockLevel, 14);
blocks.set("0,0,0", 4);
const off = buildBlockLight(blocks, materials(), bounds);
assert.equal(off.sourceCount, 0);
assert.ok(off.blockLevels.every((level) => level === 0));
assert.ok(off.data.every((value) => value === 0));
blocks.delete("0,0,0");
assert.equal(sample(buildBlockLight(blocks, materials(), bounds), [1, 0, 0]).blockLevel, 0);
blocks.set("0,0,0", 3);
assert.equal(sample(buildBlockLight(blocks, materials(), bounds), [1, 0, 0]).blockLevel, 14);
});
test("torch and soul colors retain their tint at distance without colored radius shifts", () => {
const warm = buildBlockLight(map([[0, 0, 0], 2]), materials(), bounds);
const cyan = buildBlockLight(map([[0, 0, 0], 5]), materials(), bounds);
const close = sample(warm, [1, 0, 0]).block, far = sample(warm, [8, 0, 0]).block;
assert.ok(close[0] > close[1] && close[1] > close[2]);
assert.ok(far[0] > far[1] && far[1] > far[2] && far[2] > 0);
assert.ok(Math.abs(close[1] / close[0] - far[1] / far[0]) < 0.02);
const soul = sample(cyan, [1, 0, 0]).block;
assert.ok(soul[2] > soul[1] && soul[1] > soul[0]);
assert.deepEqual(blockEmission({ state: "minecraft:torch" }).level, 0);
const lampTint = blockEmission(lamp).color;
assert.ok(lampTint[1] > 0.7, "a redstone lamp casts warm white light, not red torch light");
});
test("multiple lights combine by maximum level, never by additive strength", () => {
const mats = materials();
mats.set(5, { ...mats.get(5), light: 14 });
const field = buildBlockLight(map([[-2, 0, 0], 2], [[2, 0, 0], 5]), mats, bounds);
assert.equal(field.sourceCount, 2);
assert.equal(sample(field, [0, 0, 0]).blockLevel, 12);
const middle = sample(field, [0, 0, 0]).block;
assert.ok(middle[0] > 0.4 && middle[2] > 0.4);
assert.equal(Math.max(...field.blockLevels), 14);
});
test("direct sky stays 15 down clear columns and a full roof leaves darkness below", () => {
const open = buildBlockLight(new Map(), materials(), bounds);
assert.ok(open.skyLevels.every((level) => level === 15));
const blocks = new Map();
for (let x = -16; x <= 16; x++) for (let z = -3; z <= 3; z++) blocks.set(`${x},2,${z}`, 1);
const covered = buildBlockLight(blocks, materials(), bounds);
assert.equal(sample(covered, [0, 3, 0]).skyLevel, 15);
assert.equal(sample(covered, [0, 2, 0]).skyLevel, 0);
assert.equal(sample(covered, [0, 1, 0]).skyLevel, 0);
assert.equal(sample(covered, [0, -3, 0]).skyLevel, 0);
});
test("sky spreads laterally under an overhang with one level of decay per cell", () => {
const blocks = new Map();
for (let x = 0; x <= 16; x++) for (let z = -3; z <= 3; z++) blocks.set(`${x},2,${z}`, 1);
const field = buildBlockLight(blocks, materials(), bounds);
assert.equal(sample(field, [-1, 1, 0]).skyLevel, 15);
assert.equal(sample(field, [0, 1, 0]).skyLevel, 14);
assert.equal(sample(field, [1, 1, 0]).skyLevel, 13);
// The open exterior column remains level 15 at this height as well.
assert.equal(sample(field, [2, 0, 0]).skyLevel, 12);
});
test("slab face geometry blocks downwards sky despite its zero dampening", () => {
const mats = materials(), slab = { min: [0, 0, 0], max: [1, 0.5, 1] };
mats.set(8, { state: "minecraft:stone_slab[type=bottom]", render: [slab], light: 0, light_dampening: 0, light_occlusion: [slab] });
const blocks = new Map();
for (let x = -16; x <= 16; x++) for (let z = -3; z <= 3; z++) blocks.set(`${x},2,${z}`, 8);
const field = buildBlockLight(blocks, mats, bounds);
assert.equal(sample(field, [0, 2, 0]).skyLevel, 15);
assert.equal(sample(field, [0, 1, 0]).skyLevel, 0);
});
test("complementary slab faces close their shared boundary exactly", () => {
const bottom = { min: [0, 0, 0], max: [1, 0.5, 1] }, top = { min: [0, 0.5, 0], max: [1, 1, 1] };
const mats = materials();
mats.set(8, { state: "minecraft:stone_slab[type=bottom]", render: [bottom], light: 0, light_dampening: 0, light_occlusion: [bottom] });
mats.set(9, { state: "minecraft:stone_slab[type=top]", render: [top], light: 0, light_dampening: 0, light_occlusion: [top] });
const narrow = { min: [-2, 0, 0], max: [2, 0, 0] };
const closed = buildBlockLight(map([[-2, 0, 0], 2], [[-1, 0, 0], 8], [[0, 0, 0], 9]), mats, narrow);
assert.equal(sample(closed, [-1, 0, 0]).blockLevel, 13);
assert.equal(sample(closed, [0, 0, 0]).blockLevel, 0);
assert.equal(sample(closed, [1, 0, 0]).blockLevel, 0);
const open = buildBlockLight(map([[-2, 0, 0], 2], [[-1, 0, 0], 8], [[0, 0, 0], 8]), mats, narrow);
assert.equal(sample(open, [1, 0, 0]).blockLevel, 11);
});
test("negative coordinates and chunk boundaries preserve level and indexing", () => {
const region = { min: [-20, -2, -2], max: [20, 2, 2] };
const field = buildBlockLight(map([[-16, 0, 0], 3], [[16, 0, 0], 3]), materials(), region);
assert.equal(sample(field, [-17, 0, 0]).blockLevel, 14);
assert.equal(sample(field, [-15, 0, 0]).blockLevel, 14);
assert.equal(sample(field, [15, 0, 0]).blockLevel, 14);
assert.equal(sample(field, [17, 0, 0]).blockLevel, 14);
assert.deepEqual(field.sample([-21, 0, 0]), { block: [0, 0, 0], sky: 0, blockLevel: 0, skyLevel: 0 });
});
test("face samples use the outward hemisphere and cannot pull light through a solid roof", () => {
const blocks = map([[0, 3, 0], 3]);
for (let x = -16; x <= 16; x++) for (let z = -3; z <= 3; z++) blocks.set(`${x},2,${z}`, 1);
const field = buildBlockLight(blocks, materials(), bounds);
const bottom = [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]];
const underside = field.sampleFace([0, 2, 0], unitBox, [0, -1, 0], bottom);
assert.deepEqual(underside.block, [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]);
assert.deepEqual(underside.sky, [0, 0, 0, 0]);
const top = field.sampleFace([1, 2, 0], unitBox, [0, 1, 0], [[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]]);
assert.ok(top.block.some((rgb) => rgb[0] > 0));
assert.ok(top.sky.every((value) => value === 1));
});
test("worker transfer can restore sampling without propagating the field again", () => {
const blocks = map([[0, 0, 0], 2]), mats = materials();
const field = buildBlockLight(blocks, mats, bounds);
const raw = { min: field.min, size: field.size, data: field.data, blockLevels: field.blockLevels, skyLevels: field.skyLevels, sourceCount: field.sourceCount };
const transferred = structuredClone(raw, { transfer: [raw.data.buffer, raw.blockLevels.buffer, raw.skyLevels.buffer] });
const hydrated = attachBlockLightSamplers(transferred, blocks, mats);
assert.equal(sample(hydrated, [1, 0, 0]).blockLevel, 13);
assert.ok(sample(hydrated, [1, 0, 0]).block[0] > 0);
});
test("invalid or excessive bounds fail before allocating a field", () => {
for (const invalid of [null, { min: [0, 0, 0], max: [1, 1, 0.5] },
{ min: [1, 0, 0], max: [0, 1, 1] }, { min: [0, 0, 0], max: [1024, 1024, 1024] }])
assert.throws(() => buildBlockLight(new Map(), materials(), invalid), RangeError);
const field = buildBlockLight(new Map(), materials(), { min: [0, 0, 0], max: [0, 0, 0] });
assert.equal(field.data.length, 4);
assert.equal(field.skyLevels[0], 15);
});
test("78 directional crossings match measured original Java 26.2 attenuation", () => {
const fixture = JSON.parse(readFileSync(new URL("./fixtures/lighting-java26.2.json", import.meta.url)));
const examples = new Map(fixture.examples.map((entry) => [entry.state, entry]));
const directions = { east: [1, 0, 0], west: [-1, 0, 0], up: [0, 1, 0], down: [0, -1, 0], south: [0, 0, 1], north: [0, 0, -1] };
const measured = (state, light) => {
const entry = examples.get(state);
return { state, light, render: [], light_dampening: entry.dampening,
light_occlusion: entry.occlusion.map((box) => ({ min: box.slice(0, 3), max: box.slice(3, 6) })) };
};
assert.equal(fixture.crossings.length, 78);
for (const crossing of fixture.crossings) {
const target = directions[crossing.direction];
const mats = new Map([[1, measured(crossing.from, 15)], [2, measured(crossing.to, 0)]]);
const field = buildBlockLight(map([[0, 0, 0], 1], [target, 2]), mats,
{ min: target.map((value) => Math.min(0, value)), max: target.map((value) => Math.max(0, value)) });
assert.equal(sample(field, target).blockLevel, Math.max(0, 15 - crossing.block_dampening),
`${crossing.from} -> ${crossing.to}, ${crossing.direction}`);
}
});