diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 8dec1b6..548a2ac 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -364,6 +364,15 @@ above, including:
tick, nearest first (a Huge world is fully started after 17 s), and flying toward areas still
waiting starts them. Nothing is skipped: the same settling happens, spread out.
+- **Sloped walls and diagonal wall lines (t36, 2026-10-25).** The owner wanted funnel walls that
+ material slides down instead of catching on block steps. A Sloped wall is a half-block triangle
+ with a flat 45° face, in four orientations, built of any wall material at half a wall block's
+ cost. Dragging walls or slopes at about 45° lays a diagonal line; for slopes the drag direction
+ picks the orientation (solid side below) and T flips it, or turns a single piece. Each slope
+ 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.
+
## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble.
diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md
index 3e67937..5f32336 100644
--- a/docs/PROJECT_MAP.md
+++ b/docs/PROJECT_MAP.md
@@ -205,10 +205,10 @@ 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, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
-### src/sim/pieces.js (143 lines)
+### src/sim/pieces.js (158 lines)
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
-- functions: 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, 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
+- 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
### src/sim/economy.js (152 lines)
Money, prices, market saturation, the trade dock, buyer and builder orders.
@@ -269,9 +269,9 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
-### src/sim/blocks.js (156 lines)
+### src/sim/blocks.js (208 lines)
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
-- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
+- functions: blockOf, blockCellList, pieceCost, slopeSeal, slopeSealOwner, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (317 lines)
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal.
@@ -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.
- functions: builtForLight, computeLight
-### src/render/world.js (321 lines)
+### src/render/world.js (330 lines)
Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld
- variables: TUBE_CLASS
@@ -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 (330 lines)
+### src/render/smooth-shaders.js (337 lines)
Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS
@@ -430,14 +430,14 @@ 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 (104 lines)
+### src/ui/build-menu.js (105 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 (96 lines)
+### src/ui/selection.js (107 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
+- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope
### src/ui/selection-pickers.js (59 lines)
Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds
@@ -467,9 +467,9 @@ Gamepad input.
- functions: padPressed, pollPad
- variables: PAD
-### src/ui/previews.js (192 lines)
+### src/ui/previews.js (203 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
-- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews
+- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
### src/ui/preview-drill.js (104 lines)
Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking. Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines run on unbroken from one cell to the next and look like real fractures, not a pattern.
@@ -517,6 +517,7 @@ The frame loop and boot.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
+- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
@@ -544,3 +545,4 @@ The frame loop and boot.
- `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/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.
+- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
diff --git a/docs/agent-log.md b/docs/agent-log.md
index 5f0b0e7..2d8d608 100644
--- a/docs/agent-log.md
+++ b/docs/agent-log.md
@@ -4,6 +4,17 @@ 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: t36: sloped walls and diagonal wall lines (Claude Code, Codex)
+
+- `BP_SLOPE_BL`..`BP_SLOPE_TR` (15-18) in `src/sim/pieces.js` (`isSlope`, `isWall`, `slopeCell`,
+ `slopeCode`); `src/sim/blocks.js`: half cost and refund, `slopeSeal` corner cell and
+ `slopeSealOwner`, `diagonalDrag` and `blockLine(.., diagonal)`. Build menu, T key
+ (`turnSlope`), previews, copy/paste, undo, blueprints, hover, both looks.
+- `test/t_slopes.js`: funnel 400/400 sand (square steps 379/400), water 400/400, no leaks in all
+ four orientations from both sides, bars slide, every wall material; play.py menu and diagonal
+ drag. Seeds unchanged. `run_all.py --browser` ALL PASSED. Screenshots
+ `shots/t36-funnel-*.png`, scene `tools/scenes/t36-funnel.js`.
+
## 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
diff --git a/src/render/smooth-shaders.js b/src/render/smooth-shaders.js
index 7fca332..ff5e9a3 100644
--- a/src/render/smooth-shaders.js
+++ b/src/render/smooth-shaders.js
@@ -271,7 +271,7 @@ void main() {
}
C = tc;
}
- else if (nc == 6) {
+ else if (nc == 6 || (nc >= 15 && nc <= 18)) {
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
float e = min(min(lb.x, lb.y), min(rb.x, rb.y));
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
@@ -279,7 +279,14 @@ void main() {
float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y));
tc *= 1.0 + 0.2 * hl - 0.26 * shd;
tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
- C = tc;
+ if (nc >= 15) {
+ vec2 sl = lb;
+ if (nc == 16 || nc == 18) sl.x = 4.0 - sl.x;
+ if (nc >= 17) sl.y = 4.0 - sl.y;
+ float d = (sl.y - sl.x) * 0.70710678;
+ tc *= 1.0 + 0.2 * (1.0 - smoothstep(0.0, 0.6, d));
+ C = mix(C, tc, smoothstep(-px * 0.5, px * 0.5, d));
+ } else C = tc;
}
else if (nc == 13 || nc == 14) {
diff --git a/src/render/world.js b/src/render/world.js
index bd4c611..87c582f 100644
--- a/src/render/world.js
+++ b/src/render/world.js
@@ -145,9 +145,11 @@ function drawWorld() {
r += lift; g += lift; b += lift * 1.08;
}
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
- if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) {
+ if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) {
const lx = x & 3, ly = y & 3;
- if (lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
+ const wc = blockP[(y >> 2) * BW + (x >> 2)];
+ const edge = isSlope(wc) && (wc === BP_SLOPE_BL || wc === BP_SLOPE_TR ? lx === ly : lx + ly === 3);
+ if (edge || lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
}
}
@@ -226,11 +228,17 @@ function drawWorld() {
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
else if (m !== EMPTY) {
const ph = PH[m];
- if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) ? 6 : 1;
+ if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) ? 6 : 1;
else if (ph === PH_POWDER) cl = 2;
else if (ph === PH_LIQUID) cl = 3;
else cl = 5;
}
+ const wallCode = blockP[(y >> 2) * BW + (x >> 2)];
+ if (isSlope(wallCode) && ((m === EMPTY && !slopeCell(wallCode, x & 3, y & 3)) || (PH[m] === PH_SOLID && !(flags[i] & F_NAT)))) {
+ cl = wallCode;
+ const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
+ r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
+ }
top = cl << 24;
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0);
}
@@ -279,8 +287,9 @@ function drawWorld() {
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
let tr = 90, tg = 190, tb = 230;
if (blocked) { tr = 230; tg = 70; tb = 60; }
- else if (code === BP_WALL && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
+ else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
for (let dy = 0; dy < BS; dy++) for (let dx = 0; dx < BS; dx++) {
+ if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
const k = sy * bw + sx, c = px[k];
diff --git a/src/sim/blocks.js b/src/sim/blocks.js
index 74db3d8..8ba5293 100644
--- a/src/sim/blocks.js
+++ b/src/sim/blocks.js
@@ -2,28 +2,50 @@
// ---------- Blocks ----------
function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; }
function blockCellList(bx, by) { const out = []; for (let y = by * BS; y < by * BS + BS; y++) for (let x = bx * BS; x < bx * BS + BS; x++) out.push(y * W + x); return out; }
-function pieceCost(code, v) { return code === BP_WALL ? wallCost(v, BS * BS) : PIECE_BY_CODE[code].cost; }
-function canPlaceBlock(bx, by) {
+function pieceCost(code, v) { return isWall(code) ? wallCost(v, isSlope(code) ? 8 : BS * BS) : PIECE_BY_CODE[code].cost; }
+// One extra cell on the solid side seals diagonal crossings between successive triangles.
+function slopeSeal(code, bx, by) {
+ const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3);
+ const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4);
+ return y < 0 || y >= H ? -1 : y * W + x;
+}
+function slopeSealOwner(i) {
+ const x = i % W, y = (i / W) | 0, bx = x >> 2;
+ for (const by of [(y >> 2) - 1, (y >> 2) + 1]) {
+ if (by < 0 || by >= BH) continue;
+ const b = by * BW + bx, c = blockP[b];
+ if (isSlope(c) && slopeSeal(c, bx, by) === i) return b;
+ }
+ return -1;
+}
+function wallCells(code, bx, by) {
+ return blockCellList(bx, by).filter(i => !isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
+}
+function canPlaceBlock(bx, by, code = BP_WALL) {
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
if (blockP[by * BW + bx]) return 'Something is already built there';
for (const i of blockCellList(bx, by)) {
if (mach[i] || part[i]) return 'Something is already built there';
- if (PH[mat[i]] === PH_SOLID) return 'Needs open space; clear the rock first.';
+ if (PH[mat[i]] === PH_SOLID && ((flags[i] & F_NAT) || slopeSealOwner(i) < 0)) return 'Needs open space; clear the rock first.';
if (occ[i]) return 'A bar is in the way';
}
+ if (isSlope(code)) {
+ const i = slopeSeal(code, bx, by);
+ if (i >= 0 && PH[mat[i]] !== PH_SOLID && (mach[i] || part[i] || occ[i])) return 'The slope corner is blocked.';
+ }
return '';
}
// v: wall material (loose id) for walls, direction for belts and tubes
function placeBlock(code, v, bx, by, free, extra) {
- const err = canPlaceBlock(bx, by); if (err) return err;
- if (code === BP_WALL && !WALL_OF[v]) return 'Pick a wall material with R';
+ const err = canPlaceBlock(bx, by, code); if (err) return err;
+ if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R';
const cost = pieceCost(code, v);
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); }
- const cells = blockCellList(bx, by);
+ const cells = isSlope(code) ? wallCells(code, bx, by) : blockCellList(bx, by);
if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it
for (const i of cells) {
const y = (i / W) | 0, x = i % W;
- if (code === BP_WALL) setCell(i, WALL_OF[v], AMB[y], 0);
+ if (isWall(code)) setCell(i, WALL_OF[v], AMB[y], 0);
else if (code === BP_BELT) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0);
@@ -45,6 +67,10 @@ function placeBlock(code, v, bx, by, free, extra) {
rackState.set(b, s);
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
}
+ if (isSlope(code)) {
+ const i = slopeSeal(code, bx, by);
+ if (i >= 0 && PH[mat[i]] !== PH_SOLID) { evictCells([i], by * BS); setCell(i, WALL_OF[v], AMB[(i / W) | 0], 0); }
+ }
stats.placed++;
undoNote({ t: 'block+', bx, by, code });
return '';
@@ -57,7 +83,7 @@ function blockIntegrity(b) {
let n = 0;
for (const i of blockCellList(bx, by)) {
const m = mat[i], p = part[i];
- if (code === BP_WALL) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
+ if (isWall(code) && (!isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3))) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
else if (code === BP_BELT) { if (m === BELTL || m === BELTR) n++; }
else if (code === BP_SHAFT) { if (m === SHAFT) n++; }
else if (code === BP_RACK) { if (m === RACKB) n++; }
@@ -89,9 +115,11 @@ function removeBlock(bx, by) {
let n = 0, metal = 0;
for (const i of blockCellList(bx, by)) {
const m = mat[i], p = part[i];
- if (code === BP_WALL) {
+ if (isWall(code)) {
+ if (isSlope(code) && !slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3)) continue;
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m] && m !== SPRING) {
- metal += removeWallCell(i, v);
+ if (isSlope(code)) setCell(i, EMPTY, temp[i], 0);
+ else metal += removeWallCell(i, v);
n++;
}
} else if (isPipeCode(code)) {
@@ -102,8 +130,13 @@ function removeBlock(bx, by) {
if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; }
}
+ if (isSlope(code)) {
+ const cells = Math.floor(n * 8 / 10), refund = PH[v] === PH_ITEM ? Math.floor(cells / GRAINS_PER_BAR) : cells;
+ giveOrDrop(v, refund, bx * BS + 2, by * BS + 2);
+
+ }
if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2);
- else if (code !== BP_WALL && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
+ else if (!isWall(code) && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
if (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
if (isProcCode(code)) { procDirty = true; procState.delete(b); }
if (code === BP_RACK) {
@@ -112,15 +145,34 @@ function removeBlock(bx, by) {
if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y);
}
blockP[b] = 0; blockV[b] = 0;
+ const cleared = blockCellList(bx, by);
+ if (isSlope(code)) {
+ const i = slopeSeal(code, bx, by);
+ if (i >= 0) {
+ const nb = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2), nc = blockP[nb];
+ if ((!nc || (isSlope(nc) && !slopeCell(nc, (i % W) & 3, ((i / W) | 0) & 3))) && mat[i] === WALL_OF[v] && !(flags[i] & F_NAT)) setCell(i, EMPTY, temp[i], 0);
+ cleared.push(i);
+ }
+ }
+ for (const i of cleared) {
+ const owner = slopeSealOwner(i);
+ if (owner >= 0 && mat[i] === EMPTY) setCell(i, WALL_OF[blockV[owner]], AMB[(i / W) | 0], 0);
+ }
if (isPipeCode(code)) {
pipesDirty = true;
if (spill) { let k = spill.n; for (let y = by * BS + BS - 1; y >= by * BS && k > 0; y--) for (let x = bx * BS; x < bx * BS + BS && k > 0; x++) { const i = y * W + x; if (mat[i] === EMPTY) { setCell(i, spill.m, spill.T, 0); k--; } } }
}
return n;
}
-// Straight line of blocks along the dominant axis
-function blockLine(bx0, by0, bx1, by1) {
+// Wall drags near 45° snap to a diagonal; other lines follow the dominant axis.
+function diagonalDrag(dx, dy) { return dx !== 0 && dy !== 0 && Math.min(Math.abs(dx), Math.abs(dy)) >= Math.max(Math.abs(dx), Math.abs(dy)) * 0.7; }
+function blockLine(bx0, by0, bx1, by1, diagonal = false) {
const out = [];
+ const dx = bx1 - bx0, dy = by1 - by0;
+ if (diagonal && diagonalDrag(dx, dy)) {
+ for (let k = 0; k <= Math.max(Math.abs(dx), Math.abs(dy)); k++) out.push([bx0 + k * Math.sign(dx), by0 + k * Math.sign(dy)]);
+ return out;
+ }
if (Math.abs(bx1 - bx0) >= Math.abs(by1 - by0)) { const s = bx1 >= bx0 ? 1 : -1; for (let x = bx0; ; x += s) { out.push([x, by0]); if (x === bx1) break; } }
else { const s = by1 >= by0 ? 1 : -1; for (let y = by0; ; y += s) { out.push([bx0, y]); if (y === by1) break; } }
return out;
diff --git a/src/sim/blueprints.js b/src/sim/blueprints.js
index c13d68f..a6ee998 100644
--- a/src/sim/blueprints.js
+++ b/src/sim/blueprints.js
@@ -27,7 +27,7 @@ function flipBlueprint(bp) {
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
return {
v: 2, bw, bh: bp.bh, name: bp.name,
- blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, c, flipV(c, v), e] : [bw - 1 - x, y, c, flipV(c, v)]),
+ blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
machines: bp.machines.map(([t, x, y, d]) => {
const P = PIECE_BY_ID[t];
let nd = d;
diff --git a/src/sim/pieces.js b/src/sim/pieces.js
index 8d9523a..9a80808 100644
--- a/src/sim/pieces.js
+++ b/src/sim/pieces.js
@@ -5,6 +5,18 @@
// stays grain-level; only built pieces snap.
const BS = 4;
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
+const BP_SLOPE_BL = 15, BP_SLOPE_BR = 16, BP_SLOPE_TL = 17, BP_SLOPE_TR = 18;
+function isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
+function isWall(c) { return c === BP_WALL || isSlope(c); }
+function slopeCell(c, x, y) {
+ if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
+ if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
+ return y >= x;
+}
+function slopeCode(dx, dy, orientation) {
+ if (!diagonalDrag(dx, dy)) return BP_SLOPE_BL + orientation;
+ return dx * dy > 0 ? (orientation >= 2 ? BP_SLOPE_TR : BP_SLOPE_BL) : (orientation >= 2 ? BP_SLOPE_TL : BP_SLOPE_BR);
+}
function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; }
// Filters: each filter block has a set of materials and a mode. In drop mode (inv 0)
// the chosen materials fall through and everything else rides along on top; in carry
@@ -39,6 +51,8 @@ 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.' },
{ 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'],
@@ -84,6 +98,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;
// 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/sim/tools.js b/src/sim/tools.js
index 4512187..f3c51ff 100644
--- a/src/sim/tools.js
+++ b/src/sim/tools.js
@@ -34,7 +34,7 @@ function toolDig(cx, cy, r) {
const b = blockP[((y / BS) | 0) * BW + ((x / BS) | 0)];
setCell(i, to, temp[i], 0);
discover(to, x, y, m, 'dig');
- if (b === BP_WALL && !blockIntegrity(((y / BS) | 0) * BW + ((x / BS) | 0))) blockP[((y / BS) | 0) * BW + ((x / BS) | 0)] = 0;
+ if (isWall(b) && !blockIntegrity(((y / BS) | 0) * BW + ((x / BS) | 0))) removeBlock((x / BS) | 0, (y / BS) | 0);
dug++;
if ((dug & 3) === 0) emit({ t: 'debris', x, y, m });
} else digp[i] = p;
diff --git a/src/sim/undo.js b/src/sim/undo.js
index ed738d4..cdc12df 100644
--- a/src/sim/undo.js
+++ b/src/sim/undo.js
@@ -61,7 +61,7 @@ function ghostTick0() {
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
}
if (!blockInReach(bx, by)) continue;
- const err = canPlaceBlock(bx, by);
+ const err = canPlaceBlock(bx, by, code);
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
placeBlock(code, v, bx, by, false, e);
diff --git a/src/ui/actions.js b/src/ui/actions.js
index 8f1fc1a..9c691b2 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 (P.code === BP_WALL) { GAME.wallMat = v; }
+ if (isWall(code)) { GAME.wallMat = v; if (isSlope(code)) GAME.pieceDir.slope = code - BP_SLOPE_BL; }
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 & 3;
}
@@ -161,8 +161,8 @@ function primaryRelease(shift) {
return;
}
if (t === 'build' && GAME.drag) {
- const P = piece(), b = aimBlock();
- const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by);
+ const P = piece(), b = aimBlock(), code = pieceCode(P, b);
+ const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code));
const dd = dragDirFor(P, GAME.drag, b);
if (dd >= 0) GAME.pieceDir[P.id] = dd;
const v = pieceV(P), ex = pieceExtra(P);
@@ -170,12 +170,12 @@ function primaryRelease(shift) {
const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
let ghosts = 0;
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => {
- const res = placeBlocks(P.code, v, near, ex, GAME.replace);
+ const res = placeBlocks(code, v, near, ex, GAME.replace);
const g = { t: 'paste', blocks: [], machines: [], molds: [] };
for (const [bx, by] of far) {
const b = by * BW + bx;
- if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, P.code, v, ex) || ghostBlocks.has(b)) continue;
- ghostBlocks.set(b, ex ? [P.code, v, ex] : [P.code, v]); g.blocks.push([bx, by, P.code]); ghosts++;
+ if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
+ ghostBlocks.set(b, ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
}
if (ghosts) undoNote(g);
return res;
diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js
index edf1a18..8041ef9 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', 'grate', 'gate']],
+ ['Structure', ['wall', 'slope', 'grate', 'gate']],
['Moving', ['belt', 'filter', 'tube']],
['Liquids', ['pipe', 'pump', 'outlet']],
['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
@@ -10,6 +10,7 @@ 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.