diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index c5d6314..b7d2028 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -372,6 +372,10 @@ above, including:
also fills one cell past its corner so a diagonal run is sealed (grains move diagonally and
would otherwise slip through where two pieces meet). Square walls placed diagonally still
touch only at corners and leak there, like any square blocks.
+ Later the same day the owner asked for the Wall piece itself to work diagonally, the way tubes
+ do, so the separate Sloped wall piece left the build menu: a wall drag at about 45° lays slopes
+ (solid side below, T flips it during the drag), straight drags and clicks lay squares, and for a
+ single block T cycles square and the four corners. Slope ids are unchanged, so saves still load.
- **Diagonal screw tubes (t35, 2026-10-25).** Tubes run in eight directions. A drag at about 45°
lays a diagonal run pointing along the drag, and R cycles all eight for a single tube. Diagonal
diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md
index 98064af..b584c0e 100644
--- a/docs/PROJECT_MAP.md
+++ b/docs/PROJECT_MAP.md
@@ -205,7 +205,7 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti
Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_TUR, P_TDR, P_TDL, P_TUL, TUBE_PARTS, TUBE_DIRS, IS_TUBE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
-### src/sim/pieces.js (158 lines)
+### src/sim/pieces.js (156 lines)
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
- functions: isSlope, isWall, slopeCell, slopeCode, isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, BP_SLOPE_BL, BP_SLOPE_BR, BP_SLOPE_TL, BP_SLOPE_TR, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
@@ -356,9 +356,9 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
Propagated coloured light from glowing and burning cells.
- functions: builtForLight, looseForLight, computeLight
-### src/render/world.js (332 lines)
+### src/render/world.js (361 lines)
Scale colour without changing the smooth-look class.
-- functions: fogColor, drawWorld
+- functions: fogColor, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS
### src/render/machines.js (242 lines)
@@ -374,7 +374,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes
-### src/render/smooth-shaders.js (342 lines)
+### src/render/smooth-shaders.js (348 lines)
Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS
@@ -430,12 +430,12 @@ Notebook blueprints: starter designs, saved library and code sharing.
The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
-### src/ui/build-menu.js (105 lines)
+### src/ui/build-menu.js (109 lines)
The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
-### src/ui/selection.js (107 lines)
+### src/ui/selection.js (112 lines)
Tool selection: cycling, drill tuning, piece variants and costs.
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope
@@ -467,7 +467,7 @@ Gamepad input.
- functions: padPressed, pollPad
- variables: PAD
-### src/ui/previews.js (203 lines)
+### src/ui/previews.js (199 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
diff --git a/docs/agent-log.md b/docs/agent-log.md
index 87a6cc8..42c1300 100644
--- a/docs/agent-log.md
+++ b/docs/agent-log.md
@@ -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
open or not verified. Keep each entry short; the git history has the details.
+## 2026-10-25: t35/t36 follow-ups: wall drags make slopes, diagonal tube art (Claude Code, Codex)
+
+- Wall piece: a diagonal drag lays slopes (`pieceCode`/`turnSlope` in `src/ui/selection.js`), T flips
+ a drag or cycles square and four corners for one block; the Sloped wall piece is gone from the
+ menu, slope codes map to the wall piece (`src/sim/pieces.js`); old slope saves load
+ (`test/fixtures/slopes-v2.json.gz`).
+- Diagonal tubes draw as one continuous 45° pipe in both looks, overhanging empty cells at joints
+ (`src/render/world.js`, `smooth-shaders.js`). Drag ghosts lost the long side arrow; each ghost
+ block keeps its own chevron (`src/ui/previews.js`). The owner found the diagonal still meets
+ straight tubes off-centre; an elbow fix follows.
+- `test/t_slopes.js`, `t_diag_tubes.js`, `play.py`; `run_all.py --browser` ALL PASSED.
+
## 2026-10-25: t37 follow-up: grains on built things cast no shadows (Claude Code, Codex)
- The owner saw a flickering shadow under a grinder fed by a stream: grains resting on its
diff --git a/src/render/smooth-shaders.js b/src/render/smooth-shaders.js
index 60f2ac5..77b1310 100644
--- a/src/render/smooth-shaders.js
+++ b/src/render/smooth-shaders.js
@@ -252,8 +252,10 @@ void main() {
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
if (nc >= 19) {
vec2 d = nc == 19 ? vec2(1.0, -1.0) : nc == 20 ? vec2(1.0, 1.0) : nc == 21 ? vec2(-1.0, 1.0) : vec2(-1.0, -1.0);
- a = fract(dot(w, vec2(-d.y, d.x)) * 0.25) * 4.0;
- along = dot(w, d); sp2 = 1.0;
+ float offset = cellAt(tTree, c).r * 255.0 - 128.0;
+ bool rising = nc == 19 || nc == 21;
+ a = 2.0 + (offset + dot(fract(w) - 0.5, vec2(1.0, rising ? 1.0 : -1.0))) * 0.70710678;
+ along = dot(w, d) * 0.70710678; sp2 = 1.0;
}
float wall = 0.62;
vec3 tc;
@@ -263,7 +265,7 @@ void main() {
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
if (a < wall) tc *= 1.08;
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
- tc *= 1.0 - 0.3 * ring;
+ if (nc < 19) tc *= 1.0 - 0.3 * ring;
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
} else {
bool full = own.r + own.g + own.b > 0.0;
@@ -274,7 +276,11 @@ void main() {
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
}
- C = tc;
+ if (nc >= 19) {
+ bool full = own.r + own.g + own.b > 0.0;
+ if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5)));
+ C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, abs(a - 2.0)));
+ } else C = tc;
}
else if (nc == 6 || (nc >= 15 && nc <= 18)) {
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
diff --git a/src/render/world.js b/src/render/world.js
index 0be2365..d15518f 100644
--- a/src/render/world.js
+++ b/src/render/world.js
@@ -5,6 +5,27 @@ function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 1
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
const TUBE_CLASS = new Uint8Array(15); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
for (let p = P_TUR; p <= P_TUL; p++) TUBE_CLASS[p] = 19 + p - P_TUR;
+// Drawing only: the four transport lanes remain in the square block.
+function diagonalTubeBlock(x, y, smooth) {
+ const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
+ if (part[i] >= P_TUR && part[i] <= P_TUL) return own;
+ if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
+ // Look in the four adjoining blocks for a pipe crossing this empty corner.
+ for (let k = 0; k < 4; k++) {
+ const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0);
+ const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
+ if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
+ const b = by * BW + bx, v = blockV[b];
+ if (blockP[b] !== BP_TUBE || v < 4) continue;
+ const rising = v === 4 || v === 6, lx = x - bx * BS, ly = rising ? 3 - (y - by * BS) : y - by * BS;
+ if (Math.abs(lx - ly) > (smooth ? 3 : 1)) continue;
+ const side = lx + ly < 3 ? -1 : 1, nx = bx + side, ny = by + (rising ? -side : side);
+ if (nx < 0 || ny < 0 || nx >= BW || ny >= BH) continue;
+ const nb = ny * BW + nx;
+ if (blockP[nb] === BP_BELT || (blockP[nb] === BP_TUBE && (blockV[nb] < 4 || ((blockV[nb] === 4 || blockV[nb] === 6) === rising)))) return b;
+ }
+ return -1;
+}
function drawWorld() {
const bw = R.bw, bh = R.bh, px = R.px;
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
@@ -155,18 +176,26 @@ function drawWorld() {
}
}
// field parts drawn over contents
- const p = part[i];
+ let p = part[i];
+ const db = diagonalTubeBlock(x, y, sm);
+ let diagonalAcross = 0;
+ if (db >= 0) {
+ const v = blockV[db], rising = v === 4 || v === 6;
+ diagonalAcross = x - (db % BW) * BS - (rising ? 3 - (y - ((db / BW) | 0) * BS) : y - ((db / BW) | 0) * BS);
+ p = sm || Math.abs(diagonalAcross) <= 1 ? TUBE_PARTS[v] : 0;
+ if (sm) tpx[row + bx] = 128 + diagonalAcross;
+ }
if (p && sm && (IS_TUBE[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
else if (p) {
if (IS_TUBE[p]) {
const on = netSat(i) > 0;
const vert = p === P_TU || p === P_TD, diagonal = TDX[p] && TDY[p];
- const axis = diagonal ? x * TDX[p] + y * TDY[p] : vert ? y : x;
- const across = diagonal ? ((x * -TDY[p] + y * TDX[p]) & 3) : vert ? x & 3 : y & 3;
+ const axis = diagonal ? (x * TDX[p] + y * TDY[p]) >> 1 : vert ? y : x;
+ const across = diagonal ? diagonalAcross + 1 : vert ? x & 3 : y & 3;
const dir = diagonal ? -1 : (p === P_TU || p === P_TL) ? 1 : -1;
- if (across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
+ if (diagonal ? across !== 1 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
- if ((axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
+ if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
} else {
// screw flights: a diagonal stripe that walks along the tube when powered
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
diff --git a/src/sim/pieces.js b/src/sim/pieces.js
index 5eedde3..14b04f7 100644
--- a/src/sim/pieces.js
+++ b/src/sim/pieces.js
@@ -50,9 +50,7 @@ const POWER_FREE = true;
const CASH = 255; // stands for money in cost lists
const PIECES = [
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
- desc: 'A solid block. R picks what it is made of: 16 grains of rubble, lumps or chunks, or 2 bars of metal.' },
- { id: 'slope', code: BP_SLOPE_BL, name: 'Sloped wall', kind: 'block', variants: 'wall',
- desc: 'A half-block with a flat 45° face. Drag diagonally to make a funnel. R picks the material. T flips a dragged slope; without a drag, T cycles its four corners. Costs and returns 8 grains or 1 bar.' },
+ desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' },
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
@@ -98,7 +96,7 @@ const PIECES = [
];
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
-for (let c = BP_SLOPE_BR; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.slope;
+for (let c = BP_SLOPE_BL; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.wall;
// power per grain of belt / tube (a block is 16 grains)
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
const WHEEL_K = 0.32, ENGINE_K = 40;
diff --git a/src/ui/actions.js b/src/ui/actions.js
index 089b290..762478c 100644
--- a/src/ui/actions.js
+++ b/src/ui/actions.js
@@ -59,7 +59,7 @@ function pickUnderCursor() {
else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id;
- if (isWall(code)) { GAME.wallMat = v; if (isSlope(code)) GAME.pieceDir.slope = code - BP_SLOPE_BL; }
+ if (isWall(code)) { GAME.wallMat = v; GAME.pieceDir.wall = isSlope(code) ? code - BP_SLOPE_BL + 1 : 0; }
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
}
diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js
index 12da3ce..e836be5 100644
--- a/src/ui/build-menu.js
+++ b/src/ui/build-menu.js
@@ -1,7 +1,7 @@
// The build menu along the bottom: categories, piece tiles and their icons.
// ---------- Build menu ----------
const BUILD_CATS = [
- ['Structure', ['wall', 'slope', 'grate', 'gate']],
+ ['Structure', ['wall', 'grate', 'gate']],
['Moving', ['belt', 'filter', 'tube']],
['Liquids', ['pipe', 'pump', 'outlet']],
['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
@@ -10,7 +10,6 @@ const BUILD_CATS = [
const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]);
const PIECE_ICONS = {
wall: '
Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Hover a tile for its cost, full description and placement help. Your bot flies straight through anything you build.