Sloped walls with a flat 45° face, and diagonal wall lines (t36)

- New Sloped wall piece: a half-block triangle in any wall material, at half a wall's cost.
  Dragging walls or slopes at about 45° lays a diagonal line; the drag picks the slope's
  orientation and T flips or turns it.
- Each slope fills one cell past its corner, so a diagonal run is sealed against grains and water.
- New test/t_slopes.js: a slope funnel passes 400/400 sand (square steps 379/400).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:17:51 -04:00
co-authored by Claude Opus 5.5
parent 1a300bf744
commit 381019b7aa
21 changed files with 311 additions and 58 deletions
+65 -13
View File
@@ -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;
+1 -1
View File
@@ -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;
+15
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);