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:
@@ -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) {
|
||||
|
||||
+13
-4
@@ -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];
|
||||
|
||||
+65
-13
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
+6
-6
@@ -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;
|
||||
|
||||
@@ -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: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
|
||||
slope: '<path d="M3 3v18h18zM3 15h12M8 9v6"/>',
|
||||
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
|
||||
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
|
||||
belt: '<rect x="2" y="9" width="20" height="8" rx="2"/><circle cx="7" cy="13" r="2"/><circle cx="17" cy="13" r="2"/><path d="M9 5h7M14 3l2 2-2 2"/>',
|
||||
@@ -50,7 +51,7 @@ function buildVariantChips(P) {
|
||||
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
|
||||
const fc = GAME.filterCfg;
|
||||
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
|
||||
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>`
|
||||
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>${isSlope(P.code) ? '<span class="chip hot">T · turn / flip slope</span>' : ''}`
|
||||
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
|
||||
: dir ? dirChip : '';
|
||||
}
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ function hoverInfo(ax, ay) {
|
||||
if (b === BP_PUMP) t2.push(v & 4 ? 'off · press F' : 'on · F turns it off');
|
||||
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
|
||||
}
|
||||
else if (b === BP_WALL && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} block`;
|
||||
else if (isWall(b) && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} ${isSlope(b) ? 'sloped wall' : 'block'}`;
|
||||
const n = nets[netId[i]];
|
||||
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
|
||||
if (player.lens && !bar) t2.push(fmtT(temp[i]));
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ window.addEventListener('keydown', e => {
|
||||
case 'q': if (e.shiftKey && toolId() === 'build') cycleBuildCat(-1); else cycleItem(-1); break;
|
||||
case 'e': if (e.shiftKey && toolId() === 'build') cycleBuildCat(1); else cycleItem(1); break;
|
||||
case 'r': rotate(); break;
|
||||
case 't': if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
||||
case 't': if (toolId() === 'build' && isSlope(piece().code)) turnSlope(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
||||
case 'f': doInteract(); break;
|
||||
case 'g': GAME.replace = true; hudDirty = true; break;
|
||||
case 'h': toggleLens(); break;
|
||||
|
||||
+3
-3
@@ -43,8 +43,8 @@ function renderNotebook() {
|
||||
let h = '<p class="note">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.</p><div class="cards">';
|
||||
for (const P of PIECES) {
|
||||
if (P.hidden) continue;
|
||||
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R cycles, F opens the menu)` : P.code === BP_WALL ? '16 grains of rubble, lumps or chunks, or 2 bars' : costText(P.cost, 1);
|
||||
const afford = P.kind === 'mold' || P.code === BP_WALL ? true : canAfford(P.cost, 1);
|
||||
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R cycles, F opens the menu)` : isWall(P.code) ? (isSlope(P.code) ? '8 grains of rubble, lumps or chunks, or 1 bar' : '16 grains of rubble, lumps or chunks, or 2 bars') : costText(P.cost, 1);
|
||||
const afford = P.kind === 'mold' || isWall(P.code) ? true : canAfford(P.cost, 1);
|
||||
const size = P.kind === 'machine' ? `${P.bw}×${P.bh}` : P.kind === 'mold' ? `${MOLDS[P.mold].bw}×${MOLDS[P.mold].bh}` : '1×1';
|
||||
h += `<div class="card"><div class="h"><span>${esc(P.name)}</span><span class="chip ${afford ? 'ok' : ''}">${P.power ? esc(P.power + ' power') : P.id === 'wheel' || P.id === 'engine' ? 'makes power' : size + (size === '1×1' ? ' block' : ' blocks')}</span></div><div class="p">${esc(P.desc)}</div><div class="p">Cost: ${esc(cost)}</div></div>`;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ function renderNotebook() {
|
||||
function controlsHtml() {
|
||||
const kb = [
|
||||
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
||||
['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'],
|
||||
['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Sloped wall: turn · flip while dragging', 'T'], ['Wall or mold material menu (Build)', 'F'],
|
||||
['Pick up selection or what you click (Pneumatic nozzle)', 'Left drag · Shift: everything'], ['Select (Pneumatic nozzle)', 'Click a bag name · Q/E'], ['Add to selection (Pneumatic nozzle)', 'Ctrl+click a bag name'], ['Show everything found (Pneumatic nozzle)', 'Hold Ctrl in the bag'], ['Clear selection (Pneumatic nozzle)', 'Clear in the bag'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
||||
['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and tube directions', 'I'], ['Bot lamp', 'L'],
|
||||
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'Shift + M'], ['Full map', 'M'], ['Minimap zoom', '+ / − on minimap'],
|
||||
|
||||
@@ -72,7 +72,7 @@ function drawPastePreview() {
|
||||
const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh);
|
||||
ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]);
|
||||
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; ctx.fillRect(bx2, by2, bw2, bh2); }
|
||||
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2); }
|
||||
for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
||||
for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
||||
ctx.restore();
|
||||
|
||||
+19
-8
@@ -14,7 +14,7 @@ function drawGrid(cbx, cby, radius) {
|
||||
ctx.restore();
|
||||
}
|
||||
function previewColor(code, v) {
|
||||
if (code === BP_WALL) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; }
|
||||
if (isWall(code)) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; }
|
||||
if (code === BP_FILTER) return '96,100,110';
|
||||
return { [BP_BELT]: '110,114,124', [BP_TUBE]: '120,132,150', [BP_SHAFT]: '160,118,70', [BP_GRATE]: '170,90,66', [BP_GATE]: '160,84,64', [BP_PIPE]: '176,112,72', [BP_PUMP]: '104,122,142', [BP_OUTLET]: '170,106,66' }[code] || '120,200,190';
|
||||
}
|
||||
@@ -100,28 +100,28 @@ function drawPreviews() {
|
||||
if (reachOk && (blowing || GAME.lmb)) drawAirStreaks(ax, ay, p, r, blowing);
|
||||
}
|
||||
} else if (t === 'build') {
|
||||
const P = piece(), b = aimBlock();
|
||||
const P = piece(), b = aimBlock(), code = pieceCode(P, b);
|
||||
drawGrid(b.bx, b.by, 9);
|
||||
let cells = [], col = '120,200,190';
|
||||
if (P.kind === 'block') {
|
||||
const v = pieceV(P);
|
||||
col = previewColor(P.code, v);
|
||||
cells = GAME.drag ? (modShift() ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by)) : [[b.bx, b.by]];
|
||||
col = previewColor(code, v);
|
||||
cells = GAME.drag ? (modShift() ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code))) : [[b.bx, b.by]];
|
||||
const dd = GAME.drag ? dragDirFor(P, GAME.drag, b) : -1;
|
||||
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
|
||||
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
|
||||
const afford = canAfford(pieceCostNow(P), cells.length);
|
||||
for (const [bx, by] of cells) {
|
||||
const err = canPlaceBlock(bx, by), rb = by * BW + bx;
|
||||
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, P.code, v, pieceExtra(P));
|
||||
const err = canPlaceBlock(bx, by, code), rb = by * BW + bx;
|
||||
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P));
|
||||
const ghost = !blockInReach(bx, by) && (!err || /bar/.test(err)); // out of reach: it will be a ghost
|
||||
const ok = (!err || swap) && !ghost && afford;
|
||||
const [x, y, w, h] = blockRectPx(bx, by, 1, 1);
|
||||
ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)';
|
||||
ctx.fillRect(x, y, w, h);
|
||||
previewBlock(code, x, y, w, h);
|
||||
ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25);
|
||||
if (ghost) ctx.setLineDash([zp, zp * 0.7]);
|
||||
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1); ctx.setLineDash([]);
|
||||
previewBlock(code, x, y, w, h, true); ctx.setLineDash([]);
|
||||
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
|
||||
// a chevron on every block, pointing the way it will carry (or pump, or pour)
|
||||
const [dx, dy] = P.id === 'tube' ? [[0, -1], [1, 0], [0, 1], [-1, 0]][dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
||||
@@ -190,3 +190,14 @@ function drawPreviews() {
|
||||
if (t === 'build' && GAME.replace && !sel && !pasting) drawTag('Replace', '255,190,90', p.x + BS * zp * 1.2, p.y + BS * zp * 1.4);
|
||||
drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2);
|
||||
}
|
||||
|
||||
function previewBlock(code, x, y, w, h, stroke = false) {
|
||||
const ctx = R.ctx;
|
||||
if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; }
|
||||
const right = code === BP_SLOPE_BR || code === BP_SLOPE_TR, above = code >= BP_SLOPE_TL;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + (right ? w : 0), y + (above ? 0 : h));
|
||||
ctx.lineTo(x + (right ? w : 0), y + (above ? h : 0));
|
||||
ctx.lineTo(x + (right ? 0 : w), y + (above ? 0 : h)); ctx.closePath();
|
||||
if (stroke) ctx.stroke(); else ctx.fill();
|
||||
}
|
||||
|
||||
+13
-2
@@ -82,7 +82,7 @@ function setBrush(d) {
|
||||
// The block variant a piece is placed with: wall material, or direction (a filter's
|
||||
// materials and mode travel separately, as GAME.filterCfg)
|
||||
function pieceV(P) {
|
||||
if (P.code === BP_WALL) return GAME.wallMat;
|
||||
if (isWall(P.code)) return GAME.wallMat;
|
||||
if (P.filter) return (GAME.pieceDir[P.id] || 0) << 7;
|
||||
return GAME.pieceDir[P.id] || 0;
|
||||
}
|
||||
@@ -91,6 +91,17 @@ function filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1
|
||||
function chipCost(cost) { const ok = canAfford(cost, 1); return `<span class="chip ${ok ? 'ok' : 'bad'}">${esc(costText(cost, 1))}</span>`; }
|
||||
function pieceCostNow(P, m = GAME.wallMat) {
|
||||
if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls);
|
||||
if (P.kind === 'block' && P.code === BP_WALL) return wallCost(m, BS * BS);
|
||||
if (P.kind === 'block' && isWall(P.code)) return pieceCost(P.code, m);
|
||||
return P.cost;
|
||||
}
|
||||
|
||||
function pieceCode(P, end = aimBlock()) {
|
||||
if (!isSlope(P.code)) return P.code;
|
||||
const a = GAME.drag || end;
|
||||
return slopeCode(end.bx - a.bx, end.by - a.by, GAME.pieceDir.slope || 0);
|
||||
}
|
||||
function turnSlope() {
|
||||
const n = GAME.pieceDir.slope || 0;
|
||||
GAME.pieceDir.slope = GAME.drag ? n ^ 2 : (n + 1) % 4;
|
||||
hudDirty = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user