diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 8a6281c..c871cdf 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -352,9 +352,9 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab - functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont - variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B -### src/render/light.js (95 lines) +### src/render/light.js (103 lines) Propagated coloured light from glowing and burning cells. -- functions: builtForLight, computeLight +- functions: builtForLight, onBeltForLight, computeLight ### src/render/world.js (332 lines) Scale colour without changing the smooth-look class. diff --git a/docs/agent-log.md b/docs/agent-log.md index ef15f3f..ea47cc2 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,14 @@ What agents have done, newest first. Add an entry for every piece of work: the d note or request it was for, what changed, the files touched, the tests run, and anything left open or not verified. Keep each entry short; the git history has the details. +## 2026-10-25: t37 follow-up: belt loads cast no shadows (Claude Code, Codex) + +- The owner saw grains riding a belt overhead still shading the ground. `onBeltForLight(i)` in + `src/render/light.js` walks down through up to 12 loose cells to a belt or filter; such cells + are skipped by the light roof and pass light like air. `builtForLight` is unchanged, so the t33 + loose-grain shading still applies to belt loads. Heaps on rock, silos and wall floors still shade. +- `test/t_light.js` (56 checks); `run_all.py --browser` ALL PASSED. + ## 2026-10-25: t35: diagonal screw tubes (Claude Code, Codex) - Parts `P_TUR`..`P_TUL` (11-14), `TUBE_PARTS`, and an `IS_TUBE` table replacing every diff --git a/src/render/light.js b/src/render/light.js index eeca94a..089c667 100644 --- a/src/render/light.js +++ b/src/render/light.js @@ -5,6 +5,14 @@ function builtForLight(i, x, y) { return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i]; } +function onBeltForLight(i) { + for (let depth = 0; depth <= 12 && i < N; depth++, i += W) { + const m = mat[i], ph = PH[m]; + if (m === BELTL || m === BELTR || part[i] === P_FILTER) return true; + if (ph !== PH_POWDER && ph !== PH_LIQUID) return false; + } + return false; +} function computeLight() { const res = SET.lighting === 'fast' ? 4 : 2; const M = Math.ceil(28 / res); @@ -32,7 +40,7 @@ function computeLight() { let y = roofY[x]; for (; y < roofEnd; y++) { const i = y * W + x, ph = PH[mat[i]]; - if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y)) break; + if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y) && !((ph === PH_POWDER || ph === PH_LIQUID) && onBeltForLight(i))) break; } lightRoof[lx] = y; } @@ -42,7 +50,7 @@ function computeLight() { const wx = (lx0 + lx) * res, k = ly * lw + lx; if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; } const i = wy * W + wx, m = mat[i], ph = PH[m]; - la[k] = builtForLight(i, wx, wy) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol; + la[k] = builtForLight(i, wx, wy) || ((ph === PH_POWDER || ph === PH_LIQUID) && onBeltForLight(i)) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol; let r = 0, g = 0, b = 0; if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; } else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; } diff --git a/test/t_light.js b/test/t_light.js index b25acb6..5ab3c51 100644 --- a/test/t_light.js +++ b/test/t_light.js @@ -60,4 +60,48 @@ 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();