Fog of war: unexplored is black, fades ease into it with no stepped rim (t17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,7 @@ above, including:
|
|||||||
the ground, so the owner couldn't see what was around them or where to dig.
|
the ground, so the owner couldn't see what was around them or where to dig.
|
||||||
As tuned: clear to 12 grains, fading out by 24, with a grainy edge; the owner found the first
|
As tuned: clear to 12 grains, fading out by 24, with a grainy edge; the owner found the first
|
||||||
version's narrow fade too sharp.
|
version's narrow fade too sharp.
|
||||||
|
Unexplored ground is pure black, not dark grey (the owner: "should be black anyway").
|
||||||
|
|
||||||
## Ideas not built yet
|
## Ideas not built yet
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -221,7 +221,7 @@ World state: the per-cell typed arrays, alloc(), ambient temperature by depth, w
|
|||||||
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
||||||
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
||||||
|
|
||||||
### src/sim/fog.js (51 lines)
|
### src/sim/fog.js (52 lines)
|
||||||
Exploration only: never changes grains or the random stream.
|
Exploration only: never changes grains or the random stream.
|
||||||
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
||||||
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
||||||
@@ -343,7 +343,7 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
|||||||
Propagated coloured light from glowing and burning cells.
|
Propagated coloured light from glowing and burning cells.
|
||||||
- functions: computeLight
|
- functions: computeLight
|
||||||
|
|
||||||
### src/render/world.js (299 lines)
|
### src/render/world.js (304 lines)
|
||||||
Scale colour without changing the smooth-look class.
|
Scale colour without changing the smooth-look class.
|
||||||
- functions: fogColor, drawWorld
|
- functions: fogColor, drawWorld
|
||||||
- variables: TUBE_CLASS
|
- variables: TUBE_CLASS
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ 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
|
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.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-24: t17 follow-up: no jagged rim, unexplored is black (Claude Code)
|
||||||
|
|
||||||
|
- The owner still saw a jagged grey edge. Causes found by sampling pixels: the fade went to 0
|
||||||
|
(black) while unexplored was drawn (12, 10, 10), leaving a dark stepped rim; the ±40 noise lit
|
||||||
|
the faintest cells; rays that reached rock already dim dropped to 0 in 2–3 grains.
|
||||||
|
- `src/render/world.js`: unexplored and off-map are pure black (`OUT`, smooth-look buffers);
|
||||||
|
partial fog eases into black along a smoothstep, with noise scaled by s·(1−s). `src/sim/fog.js`:
|
||||||
|
sight into rock fades over 8 grains as a fraction of its brightness.
|
||||||
|
- `test/play.py`: the three unexplored-colour checks expect black (owner's decision).
|
||||||
|
`docs/systems/fog.md` updated. Screenshots `shots/fog-dig-black-{pixel,smooth,nolight}.png`.
|
||||||
|
- Tests: `run_all.py --browser` passed except `t_smooth` once at load 7 (passed on rerun).
|
||||||
|
|
||||||
## 2026-10-24: t17 follow-up: softer fog edges (Claude Code, Codex)
|
## 2026-10-24: t17 follow-up: softer fog edges (Claude Code, Codex)
|
||||||
|
|
||||||
- The owner found the edges sharp and stepped. `src/sim/fog.js`: sense ring 24 grains with a
|
- The owner found the edges sharp and stepped. `src/sim/fog.js`: sense ring 24 grains with a
|
||||||
|
|||||||
+4
-4
@@ -28,10 +28,10 @@ immediately from the bot.
|
|||||||
|
|
||||||
## Drawing and setting
|
## Drawing and setting
|
||||||
|
|
||||||
Both looks and the thermal lens sample fog bilinearly. Partial values get stable per-grain
|
Both looks and the thermal lens sample fog bilinearly. Partly seen grains darken toward black
|
||||||
noise of about ±40, clamped to 1–255, to break up stepped edges. Unseen and fully seen
|
along a smoothstep curve, with stable per-grain noise (up to about ±38 mid-fade, none at either
|
||||||
grains are unchanged. Seen grains use the usual drawing;
|
end) to break up the half-resolution steps. Fully seen grains use the usual drawing; unseen
|
||||||
unseen grains use the out-of-world colour and smooth-look buffers, hiding their shapes.
|
grains are pure black (as is everything off the map) in both looks, hiding their shapes.
|
||||||
Partial values dim foreground, background and trees. Bloom sources need at least 128;
|
Partial values dim foreground, background and trees. Bloom sources need at least 128;
|
||||||
bars, flyers, rain and particles need at least 64. Hover info and the cursor label say
|
bars, flyers, rain and particles need at least 64. Hover info and the cursor label say
|
||||||
only “Unexplored” below 64.
|
only “Unexplored” below 64.
|
||||||
|
|||||||
+11
-6
@@ -23,7 +23,7 @@ function drawWorld() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const useFog = SET.fog;
|
const useFog = SET.fog;
|
||||||
const OUT = sm ? 0x040a0a0c : 0xff0a0a0c;
|
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
|
||||||
const lighting = SET.lighting !== 'off' && !R.lens;
|
const lighting = SET.lighting !== 'off' && !R.lens;
|
||||||
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
|
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
|
||||||
const grain = SET.grain, incand = SET.incandescence;
|
const grain = SET.grain, incand = SET.incandescence;
|
||||||
@@ -35,22 +35,27 @@ function drawWorld() {
|
|||||||
for (let by = 0; by < bh; by++) {
|
for (let by = 0; by < bh; by++) {
|
||||||
const y = vy0 + by;
|
const y = vy0 + by;
|
||||||
const row = by * bw;
|
const row = by * bw;
|
||||||
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
||||||
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
||||||
const yw = y * W;
|
const yw = y * W;
|
||||||
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
||||||
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
||||||
for (let bx = 0; bx < bw; bx++) {
|
for (let bx = 0; bx < bw; bx++) {
|
||||||
const x = vx0 + bx;
|
const x = vx0 + bx;
|
||||||
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
||||||
let fv = 255;
|
let fv = 255;
|
||||||
if (useFog) {
|
if (useFog) {
|
||||||
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
|
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
|
||||||
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
|
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
|
||||||
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
|
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
|
||||||
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
||||||
// Stable grain noise breaks up the half-resolution fog's stepped edges.
|
// ease the fade into black (smoothstep) and break up the half-resolution steps with grain
|
||||||
if (fv < 255) fv = Math.max(1, Math.min(255, fv + (noiseB[yw + x] - 128) * (40 / 128)));
|
// noise that is strongest mid-fade and nothing at either end, so the outer edge has no rim
|
||||||
|
if (fv < 255) {
|
||||||
|
let s = fv / 255;
|
||||||
|
s = Math.min(1, Math.max(0, s + (noiseB[yw + x] - 128) * (0.6 / 128) * s * (1 - s)));
|
||||||
|
fv = Math.max(1, 255 * s * s * (3 - 2 * s));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||||
const T = temp[i];
|
const T = temp[i];
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@ function fogLook(cx, cy) {
|
|||||||
if (x < 0 || y < 0 || x >= W || y >= H) break;
|
if (x < 0 || y < 0 || x >= W || y >= H) break;
|
||||||
const ph = PH[mat[y * W + x]];
|
const ph = PH[mat[y * W + x]];
|
||||||
if (hit < 0 && (ph === PH_SOLID || ph === PH_POWDER)) hit = d;
|
if (hit < 0 && (ph === PH_SOLID || ph === PH_POWDER)) hit = d;
|
||||||
const v = Math.min(255, (48 - d) * 255 / 16, hit < 0 ? 255 : (8 - d + hit) * 255 / 8);
|
// into rock, sight fades out over 8 grains from however bright it arrived
|
||||||
|
const v = Math.min(255, (48 - d) * 255 / 16) * (hit < 0 ? 1 : (8 - d + hit) / 8);
|
||||||
if (v <= 0) break;
|
if (v <= 0) break;
|
||||||
fogMark(x, y, v);
|
fogMark(x, y, v);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -807,7 +807,7 @@ with sync_playwright() as p:
|
|||||||
}""")
|
}""")
|
||||||
fp.wait_for_timeout(300)
|
fp.wait_for_timeout(300)
|
||||||
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
|
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
|
||||||
check('unexplored pixel is out-of-world black', fp.evaluate(sample) == 0x0a0a0c)
|
check('unexplored pixel is black', fp.evaluate(sample) == 0)
|
||||||
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
|
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
|
||||||
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
|
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
|
||||||
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
|
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
|
||||||
@@ -821,13 +821,13 @@ with sync_playwright() as p:
|
|||||||
}"""))
|
}"""))
|
||||||
fp.screenshot(path=str(out / 'fog-pixel.png'))
|
fp.screenshot(path=str(out / 'fog-pixel.png'))
|
||||||
fp.evaluate('R.lens = true')
|
fp.evaluate('R.lens = true')
|
||||||
check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0x0a0a0c)
|
check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0)
|
||||||
fp.evaluate('R.lens = false; SET.fog = false')
|
fp.evaluate('R.lens = false; SET.fog = false')
|
||||||
check('fog toggle shows the same rock', fp.evaluate(sample) != 0x0a0a0c)
|
check('fog toggle shows the same rock', fp.evaluate(sample) != 0)
|
||||||
fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000")
|
fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000")
|
||||||
fp.wait_for_timeout(500)
|
fp.wait_for_timeout(500)
|
||||||
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
|
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
|
||||||
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x040a0a0c && R.backPx[k]===0x000a0a0c && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
|
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x04000000 && R.backPx[k]===0 && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
|
||||||
fp.screenshot(path=str(out / 'fog-smooth.png'))
|
fp.screenshot(path=str(out / 'fog-smooth.png'))
|
||||||
fb.close()
|
fb.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user