// Run the renderer's lighting against real simulation data, with only canvas storage stubbed. const fs = require('fs'); const path = require('path'); const { load, suite, flatWorld } = require('./harness'); const { run } = load(); const { check, done } = suite(); for (const f of ['setup.js', 'light.js']) run(fs.readFileSync(path.join(__dirname, '../src/render', f), 'utf8')); run(` const SET = { lighting: 'full', daylight: 1, ambient: 0.12 }; R.lightC = {}; R.lightX = { createImageData: (w, h) => ({ data: new Uint8ClampedArray(w * h * 4) }), putImageData() {} }; R.vx0 = 0; R.vy0 = 100; R.bw = 320; R.bh = 80; function lightAt(a, x, y) { return a[(Math.floor(y / R.lres) - R.ly0) * R.lw + Math.floor(x / R.lres) - R.lx0]; } `); flatWorld(run, 320, 200, 160); run(` bot.lamp = false; for (let bx = 8; bx < 30; bx++) placeBlock(BP_WALL, RSTONE, bx, 20, true); for (let by = 21; by < 39; by++) { placeBlock(BP_WALL, RSTONE, 8, by, true); placeBlock(BP_WALL, RSTONE, 29, by, true); } placeBlock(BP_BELT, 1, 12, 30, true); placeBlock(BP_TUBE, 0, 14, 30, true); addMachine('silo', 18, 30, 0); addMold('bit', 24, 30, RSTONE, true); for (let y = 80; y < 88; y++) for (let x = 180; x < 280; x++) setCell(y * W + x, STONE, AMB[y], F_NAT); updateRoof(); const simRoof = Array.from(roofY); `); for (const mode of ['full', 'fast']) { run(`SET.lighting = '${mode}'; computeLight();`); for (const channel of ['lr', 'lg']) { const [inside, open, rock, ground, openGround] = run(`[lightAt(R.${channel}, 80, 144), lightAt(R.${channel}, 152, 144), lightAt(R.${channel}, 232, 144), lightAt(R.${channel}, 80, 160), lightAt(R.${channel}, 152, 160)]`); check(`${mode} ${channel}: under a built roof matches open daylight`, Math.abs(inside / open - 1) < 0.03, `${inside} / ${open}`); check(`${mode} ${channel}: ground under the base matches outside`, Math.abs(ground / openGround - 1) < 0.03, `${ground} / ${openGround}`); check(`${mode} ${channel}: natural overhang still shades`, rock < open * 0.95, `${rock} / ${open}`); } // Directly check transmission as daylight can hide an opaque piece in the open. for (const [name, x, y] of [['wall', 32, 120], ['belt', 48, 120], ['tube', 56, 120], ['machine', 72, 120], ['mold', 96, 120]]) { const a = run(`lightAt(R.latt, ${x}, ${y})`); check(`${mode}: ${name} transmits like air`, Math.abs(a - Math.pow(0.978, mode === 'fast' ? 4 : 2)) < 1e-6, a); } } check('lighting leaves the simulation roof unchanged', run('simRoof.every((y, x) => roofY[x] === y) && roofY[80] === 80')); for (const material of ['STONE', 'SAND', 'WATER']) { run(`setCell(96 * W + 80, ${material}, AMB[96], F_NAT); updateRoof(); computeLight();`); check(`${material} above the view still blocks daylight`, run('lightAt(R.lr, 80, 144) < 0.95')); run('setCell(96 * W + 80, EMPTY, AMB[96], 0);'); } run('updateRoof(); computeLight();'); check('removing an obstruction restores daylight', run('lightAt(R.lr, 80, 144) === 1')); for (const field of ['blockP[24 * BW + 20]', 'mach[96 * W + 80]', 'part[96 * W + 80]']) { run(`setCell(96 * W + 80, STONE, AMB[96], F_NAT); ${field} = 1; updateRoof(); computeLight();`); check(`${field}: built contents do not shade or absorb light`, run('lightAt(R.lr, 80, 144) === 1 && Math.abs(lightAt(R.latt, 80, 96) - Math.pow(0.978, 4)) < 1e-6')); run(`${field} = 0; setCell(96 * W + 80, EMPTY, AMB[96], 0);`); } run('updateRoof();'); run(`SET.daylight = 0; temp[140 * W + 80] = 1500; computeLight();`); check('hot material inside the base lights its surroundings', run('lightAt(R.lr, 88, 140) > 0.2')); run(`temp[140 * W + 80] = AMB[140]; bot.x = 72; bot.y = 130; bot.lamp = true; computeLight();`); check('the bot lamp still lights the base', run('lightAt(R.lr, 80, 140) > 0.2')); flatWorld(run, 320, 200, 160); run(` SET.daylight = 1; bot.lamp = false; for (let bx = 8; bx < 30; bx++) placeBlock(BP_BELT, 1, bx, 20, true); for (let x = 32; x < 120; x++) for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0); for (let x = 180; x < 280; x++) { setCell(80 * W + x, STONE, AMB[80], F_NAT); for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0); } updateRoof(); `); for (const mode of ['full', 'fast']) { run(`SET.lighting = '${mode}'; computeLight();`); for (const channel of ['lr', 'lg', 'lb']) { const [belt, open, stone] = run(`[lightAt(R.${channel}, 80, 84), lightAt(R.${channel}, 152, 84), lightAt(R.${channel}, 232, 84)]`); check(`${mode} ${channel}: a loaded belt leaves daylight below it`, Math.abs(belt - open) < 1e-6, `${belt} / ${open}`); check(`${mode} ${channel}: sand on natural stone still shades below`, stone < open * 0.95, `${stone} / ${open}`); } run('setCell(76 * W + 80, WATER, AMB[76], 0); updateRoof(); computeLight();'); const a = run('lightAt(R.latt, 80, 76)'); check(`${mode}: a wet belt load transmits like air`, Math.abs(a - Math.pow(0.978, mode === 'fast' ? 4 : 2)) < 1e-6, a); run('setCell(76 * W + 80, EMPTY, AMB[76], 0); updateRoof();'); } for (const [name, support, depth, gap, expected] of [ ['left belt', 'mat[i] = BELTL', 12, '', true], ['right belt', 'mat[i] = BELTR', 12, '', true], ['filter', 'part[i] = P_FILTER', 12, '', true], ['deep heap', 'mat[i] = BELTR', 13, '', false], ['air gap', 'mat[i] = BELTR', 3, 'EMPTY', false], ['gas gap', 'mat[i] = BELTR', 3, 'STEAM', false], ['natural ledge', 'mat[i] = STONE; flags[i] = F_NAT', 3, '', false], ['wall floor', 'mat[i] = STONE', 3, '', false], ['tube', 'part[i] = P_TR', 3, '', false], ]) { run(` for (let y = 40; y <= 53; y++) { const i = y * W + 300; mat[i] = SAND; part[i] = 0; flags[i] = 0; } { const i = (40 + ${depth}) * W + 300; mat[i] = EMPTY; mat[i + W] = BELTR; ${support}; } ${gap ? `mat[41 * W + 300] = ${gap};` : ''} `); check(`${name}: belt load detection`, run('onBeltForLight(40 * W + 300)') === expected); } run('mat[(H - 1) * W + 300] = SAND;'); check('belt load detection stops at the world bottom', run('!onBeltForLight((H - 1) * W + 300)')); done();