Loose grains and solid ground look different: a seam on the rock, pebbly lighter grains (t33)

- Smooth look: natural ground gets a soft seam where loose grains meet it; packed grains keep a
  pebble texture and are slightly lighter. Pixel look and thermal lens: the same idea per pixel.
- play.py: the F-on-a-mold toast check records toasts the key press makes instead of counting
  the ones on screen, which failed when an older toast expired at the same moment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:59:54 -04:00
co-authored by Claude Opus 5.5
parent 20d0627d75
commit 1a300bf744
7 changed files with 108 additions and 7 deletions
+3 -2
View File
@@ -356,7 +356,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: builtForLight, computeLight - functions: builtForLight, computeLight
### src/render/world.js (308 lines) ### src/render/world.js (321 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
@@ -374,7 +374,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - variables: SM, smBytes
### src/render/smooth-shaders.js (325 lines) ### src/render/smooth-shaders.js (330 lines)
Smooth shaders: vertex and fragment programs for the rounded world look. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - variables: SM_VS, SM_FS
@@ -543,3 +543,4 @@ The frame loop and boot.
- `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view. - `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge. - `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 - `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
+13
View File
@@ -4,6 +4,19 @@ 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-25: t33: loose grains and solid ground look different (Claude Code, Codex)
- Smooth look (`src/render/smooth-shaders.js`): natural ground gets a soft seam where it meets
loose grains as well as air; packed grains keep the pebble shading and are slightly lighter.
Pixel look and thermal lens (`drawWorld()` in `src/render/world.js`): loose grains a touch
lighter and speckled, solid cells touching loose grains darker.
- Smooth frame 1.08 -> 1.10 ms. `t_smooth.py` checks the seam and the lighter grains;
`tools/scenes/t33-ground.js` for screenshots (`shots/t33/`).
- play.py: 'F on an automatic ingot mold stays quiet' counted toasts on screen, and failed
now and then when an older toast expired or a new one arrived in the same 150 ms; it now
records the toasts the key press makes (none). `run_all.py --browser` ALL PASSED apart from
that check, which passes in the new form.
## 2026-10-25: t37: buildings cast no shadows (Claude Code, Codex) ## 2026-10-25: t37: buildings cast no shadows (Claude Code, Codex)
- `computeLight()` (`src/render/light.js`): a per-view-column light roof that skips player-built - `computeLight()` (`src/render/light.js`): a per-view-column light roof that skips player-built
+7 -2
View File
@@ -176,9 +176,11 @@ void main() {
float band = 1.0 - smoothstep(0.5, 0.9, M); float band = 1.0 - smoothstep(0.5, 0.9, M);
float lit = dot(nrm, normalize(vec2(-0.5, -1.0))); float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
tc *= 1.0 + 0.42 * lit * band; tc *= 1.0 + 0.42 * lit * band;
// the outline follows where ground and grains meet open space, never where rock meets gravel // Keep the air outline; a softer, screen-width seam also separates rock from loose grains.
float dm = (M - 0.5) / fwM; float dm = (M - 0.5) / fwM;
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm)); tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
float dt = (F.r - 0.5) / fwT;
if (nc == 1 && pw > 0.0) tc *= mix(0.65, 1.0, smoothstep(0.0, 1.5, dt));
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT)); C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
} }
@@ -200,10 +202,13 @@ void main() {
} }
float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best); float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best);
if (closed && F.r < 0.5) cover = 1.0; if (closed && F.r < 0.5) cover = 1.0;
// Pebbles stop at the rock face so they cannot paint over its thin seam.
cover *= 1.0 - smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT);
if (cover > 0.0) { if (cover > 0.0) {
float r2 = min(1.0, dot(gd, gd)); float r2 = min(1.0, dot(gd, gd));
vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25)); vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25));
float sh = np == 4 ? 0.9 + 0.08 * vn(w * 0.9) : 0.8 + 0.3 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6)))); float sh = 0.98 + 0.18 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6))));
sh *= mix(1.0, 0.94, smoothstep(0.7 - px * 0.2, 1.15 + px * 0.2, best));
vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh; vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh;
if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best)); if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best));
C = mix(C, pc, cover); C = mix(C, pc, cover);
+13
View File
@@ -59,12 +59,24 @@ function drawWorld() {
} }
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];
// Also used by the thermal lens, which always draws in pixels.
let looseShade = 1;
if (!sm && m !== EMPTY && !builtForLight(i, x, y)) {
if (PH[m] === PH_POWDER) looseShade = 1.1 + nz * 0.00065;
else if (PH[m] === PH_SOLID && (
(x > 0 && PH[mat[i - 1]] === PH_POWDER && !part[i - 1]) ||
(x + 1 < W && PH[mat[i + 1]] === PH_POWDER && !part[i + 1]) ||
(y > 0 && PH[mat[i - W]] === PH_POWDER && !part[i - W]) ||
(y + 1 < H && PH[mat[i + W]] === PH_POWDER && !part[i + W])
)) looseShade = 0.72;
}
let r, g, b; let r, g, b;
if (lens) { if (lens) {
const ti = thermIdx(T); const ti = thermIdx(T);
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; } if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; } else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
const p = part[i]; const p = part[i];
r *= looseShade; g *= looseShade; b *= looseShade;
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; } if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; } if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r); px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
@@ -93,6 +105,7 @@ function drawWorld() {
else { else {
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128; const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v; r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
r *= looseShade; g *= looseShade; b *= looseShade;
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; } if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; } else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
else if (m === BELTL || m === BELTR) { else if (m === BELTL || m === BELTR) {
+7 -3
View File
@@ -695,9 +695,13 @@ with sync_playwright() as p:
pg.screenshot(path=str(out / '05-mold-ready.png')) pg.screenshot(path=str(out / '05-mold-ready.png'))
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])')) check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
pg.keyboard.press('2'); settle(60) pg.keyboard.press('2'); settle(60)
ev("GAME.status = ''; GAME.statusT = 0"); n_toasts = ev("$('toasts').children.length") ev("GAME.status = ''; GAME.statusT = 0")
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150); pg.keyboard.press('f'); settle(150) pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150)
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts) # count toasts made by the key press itself: counting the ones on screen also caught an older toast expiring
ev("window._toastsMade = []; window._toastFn = toast; toast = function (t, o) { _toastsMade.push(t); return _toastFn(t, o); }; 0")
pg.keyboard.press('f'); settle(150)
made = ev("(() => { toast = _toastFn; return _toastsMade; })()")
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and made == [], made)
ev("closeModal('m-dialog')") ev("closeModal('m-dialog')")
check('F with vacuum still uses what is in reach', ev("""(() => { check('F with vacuum still uses what is in reach', ev("""(() => {
$('toasts').innerHTML = ''; $('toasts').innerHTML = '';
+41
View File
@@ -35,6 +35,47 @@ with sync_playwright() as p:
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500) ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
ev("SM.scale = 0.5"); pg.wait_for_timeout(1500) ev("SM.scale = 0.5"); pg.wait_for_timeout(1500)
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st) st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
# Uniform colours isolate the solid/loose seam from material noise and lighting.
ground = ev('''() => {
GAME.paused = true; requestAnimationFrame = () => 0;
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = 0; SET.zoom = 6;
resizeRender(); bot.x = 138; bot.y = 118; snapCamera();
for (let y = 60; y < 220; y++) for (let x = 40; x < 240; x++) {
const i = y * W + x;
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
}
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
const at = (x, y) => { const c = R.px[(y - R.vy0) * R.bw + x - R.vx0]; return (c & 255) + (c >>> 8 & 255) + (c >>> 16 & 255); };
const out = {};
for (const lens of [false, true]) {
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
const edge = at(140, 140), inside = at(140, 142), loose = at(140, 130);
fill(STONE); drawWorld();
out[lens ? 'thermal' : 'pixel'] = [edge, inside, loose, at(140, 140)];
}
R.lens = false;
const i = 140 * W + 140;
flags[i] = 0; fill(RSTONE); drawWorld(); const built = at(140, 140);
fill(STONE); drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
const sample = (x, y) => {
const p = worldToScreenPx(x, y);
const c = R.ctx.getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
return c[0] + c[1] + c[2];
};
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
const packed = [];
for (let y = 120; y < 121; y += 1 / 6) for (let x = 140; x < 141; x += 1 / 6) packed.push(sample(x, y));
out.texture = Math.max(...packed) - Math.min(...packed);
return out;
}''')
for look in ['pixel', 'thermal']:
edge, inside, loose, solid = ground[look]
check(f'{look}: ground has a darker edge against loose grains', edge < inside * 0.85 and inside == solid, ground[look])
check(f'{look}: loose rubble is lighter than solid stone', loose > inside, ground[look])
check('built solids get no loose-grain outline', ground['built'][0] == ground['built'][1], ground['built'])
check('smooth: a thin darker seam separates ground and rubble', ground['smooth'][0] < ground['smooth'][1] * 0.9, ground['smooth'])
check('smooth: packed rubble keeps pebble shading', ground['texture'] > 30, ground['texture'])
# a browser without hardware drawing gets the pixel look and a note # a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500}) pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();') pg2.add_init_script('localStorage.clear();')
+24
View File
@@ -0,0 +1,24 @@
// Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt.
// Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
const cx = 360, cy = 300;
for (let y = 0; y < 480; y++) for (let x = 100; x < 620; x++) {
const dx = x - cx, dy = y - cy;
const floor = 20 + Math.round(3 * Math.sin(dx * 0.11));
const wall = Math.abs(dx) > 68 || dy < -42 || dy >= floor;
const ground = dx < -23 ? STONE : dx < 23 ? GRANITE : DIRT;
let m = wall ? ground : EMPTY;
if (!wall && dy > floor - 11 - Math.round(3 * Math.sin(dx * 0.18))) {
m = dx < -23 ? RSTONE : dx < 23 ? CUORE : LDIRT;
if ((x * 7 + y * 11) % 19 === 0) m = CUORE;
}
const i = y * W + x;
bgm[i] = 0;
setCell(i, m, 22, wall ? F_NAT : 0);
}
for (let x = 100; x < 620; x++) {
surfY[x] = 100; dirtY[x] = 180; graniteY[x] = 400;
}
updateRoof();
window.CAM = [cx, cy];
SET.daylight = 0;
SET.lighting = 'full';