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}`); } });