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:
+3
-2
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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 = '';
|
||||||
|
|||||||
@@ -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();')
|
||||||
|
|||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user