Wall drags at 45° lay slopes; diagonal tubes drawn as one pipe; no side arrow on drag ghosts (t35, t36)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 22:20:48 -04:00
co-authored by Claude Opus 5.5
parent f19facbe5c
commit 814b51df46
17 changed files with 172 additions and 65 deletions
+4
View File
@@ -372,6 +372,10 @@ above, including:
also fills one cell past its corner so a diagonal run is sealed (grains move diagonally and 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 would otherwise slip through where two pieces meet). Square walls placed diagonally still
touch only at corners and leak there, like any square blocks. 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° - **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 lays a diagonal run pointing along the drag, and R cycles all eight for a single tube. Diagonal
+7 -7
View File
@@ -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 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 - 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. 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 - 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 - 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. Propagated coloured light from glowing and burning cells.
- functions: builtForLight, looseForLight, computeLight - functions: builtForLight, looseForLight, computeLight
### src/render/world.js (332 lines) ### src/render/world.js (361 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld - functions: fogColor, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS - variables: TUBE_CLASS
### src/render/machines.js (242 lines) ### 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 - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - 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. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - 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. The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece - 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. The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - 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. 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 - 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 - functions: padPressed, pollPad
- variables: PAD - 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. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
+12
View File
@@ -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 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: 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) ## 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 - The owner saw a flickering shadow under a grinder fed by a stream: grains resting on its
+10 -4
View File
@@ -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; float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
if (nc >= 19) { 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); 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; float offset = cellAt(tTree, c).r * 255.0 - 128.0;
along = dot(w, d); sp2 = 1.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; float wall = 0.62;
vec3 tc; vec3 tc;
@@ -263,7 +265,7 @@ void main() {
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl); tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
if (a < wall) tc *= 1.08; 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); 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))); tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
} else { } else {
bool full = own.r + own.g + own.b > 0.0; 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 = 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)); 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)) { else if (nc == 6 || (nc >= 15 && nc <= 18)) {
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb; vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
+34 -5
View File
@@ -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 // 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; 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; 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() { function drawWorld() {
const bw = R.bw, bh = R.bh, px = R.px; const bw = R.bw, bh = R.bh, px = R.px;
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY); R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
@@ -155,18 +176,26 @@ function drawWorld() {
} }
} }
// field parts drawn over contents // 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); } } 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) { else if (p) {
if (IS_TUBE[p]) { if (IS_TUBE[p]) {
const on = netSat(i) > 0; const on = netSat(i) > 0;
const vert = p === P_TU || p === P_TD, diagonal = TDX[p] && TDY[p]; 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 axis = diagonal ? (x * TDX[p] + y * TDY[p]) >> 1 : vert ? y : x;
const across = diagonal ? ((x * -TDY[p] + y * TDX[p]) & 3) : vert ? x & 3 : y & 3; const across = diagonal ? diagonalAcross + 1 : vert ? x & 3 : y & 3;
const dir = diagonal ? -1 : (p === P_TU || p === P_TL) ? 1 : -1; 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 (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 { } else {
// screw flights: a diagonal stripe that walks along the tube when powered // screw flights: a diagonal stripe that walks along the tube when powered
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0); const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
+2 -4
View File
@@ -50,9 +50,7 @@ const POWER_FREE = true;
const CASH = 255; // stands for money in cost lists const CASH = 255; // stands for money in cost lists
const PIECES = [ const PIECES = [
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall', { 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.' }, 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: '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'], { 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.' }, 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'], { 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 = []; 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; } }); 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) // power per grain of belt / tube (a block is 16 grains)
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6; const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
const WHEEL_K = 0.32, ENGINE_K = 40; const WHEEL_K = 0.32, ENGINE_K = 40;
+1 -1
View File
@@ -59,7 +59,7 @@ function pickUnderCursor() {
else if (blockP[bi] || ghost) { else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1]; const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id; 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.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; else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
} }
+9 -5
View File
@@ -1,7 +1,7 @@
// The build menu along the bottom: categories, piece tiles and their icons. // The build menu along the bottom: categories, piece tiles and their icons.
// ---------- Build menu ---------- // ---------- Build menu ----------
const BUILD_CATS = [ const BUILD_CATS = [
['Structure', ['wall', 'slope', 'grate', 'gate']], ['Structure', ['wall', 'grate', 'gate']],
['Moving', ['belt', 'filter', 'tube']], ['Moving', ['belt', 'filter', 'tube']],
['Liquids', ['pipe', 'pump', 'outlet']], ['Liquids', ['pipe', 'pump', 'outlet']],
['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']], ['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
@@ -10,7 +10,6 @@ const BUILD_CATS = [
const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]); const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]);
const PIECE_ICONS = { const PIECE_ICONS = {
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>', 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"/>', 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"/>', 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"/>', 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"/>',
@@ -29,7 +28,12 @@ const PIECE_ICONS = {
bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>', bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>',
tankmold: '<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M9 4h6v3M8.5 12h7"/>', tankmold: '<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M9 4h6v3M8.5 12h7"/>',
}; };
function pieceIcon(id) { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${PIECE_ICONS[id] || ''}</svg>`; } function pieceIcon(id) {
const v = GAME.drag && GAME.dragDir >= 0 ? GAME.dragDir : GAME.pieceDir.tube || 0;
const body = PIECE_ICONS[id] || '';
const icon = id === 'tube' && v >= 4 ? `<g transform="rotate(${v === 4 || v === 6 ? -45 : 45} 12 12)">${body}</g>` : body;
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${icon}</svg>`;
}
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; } function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
let lastBuildKey = '', lastBuildMenu = ''; let lastBuildKey = '', lastBuildMenu = '';
function selectBuildCat(c) { function selectBuildCat(c) {
@@ -51,7 +55,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 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 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}` : ''); 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>${isSlope(P.code) ? '<span class="chip hot">T · turn / flip slope</span>' : ''}` 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>${P.code === BP_WALL ? `<span class="chip hot">${['square', '◣ lower left', '◢ lower right', '◤ upper left', '◥ upper right'][isSlope(pieceCode(P)) ? pieceCode(P) - BP_SLOPE_BL + 1 : 0]} · T · turn / flip</span>` : ''}`
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}` : P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
: dir ? dirChip : ''; : dir ? dirChip : '';
} }
@@ -63,7 +67,7 @@ function renderBuildBar() {
const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover; const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover;
const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : '']; const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : ''];
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join(''); const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join('');
const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.pieceDir[P.id], GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), innerWidth, innerHeight, SET.uiScale].join('|'); const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.pieceDir[P.id], GAME.dragDir, GAME.previewCode, GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), innerWidth, innerHeight, SET.uiScale].join('|');
if (key === lastBuildKey) return; if (key === lastBuildKey) return;
lastBuildKey = key; lastBuildKey = key;
let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('') let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('')
+1 -1
View File
@@ -30,7 +30,7 @@ window.addEventListener('keydown', e => {
case 'q': if (e.shiftKey && toolId() === 'build') cycleBuildCat(-1); else cycleItem(-1); break; 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 'e': if (e.shiftKey && toolId() === 'build') cycleBuildCat(1); else cycleItem(1); break;
case 'r': rotate(); break; case 'r': rotate(); break;
case 't': if (toolId() === 'build' && isSlope(piece().code)) turnSlope(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break; case 't': if (toolId() === 'build' && piece().code === BP_WALL) turnSlope(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
case 'f': doInteract(); break; case 'f': doInteract(); break;
case 'g': GAME.replace = true; hudDirty = true; break; case 'g': GAME.replace = true; hudDirty = true; break;
case 'h': toggleLens(); break; case 'h': toggleLens(); break;
+3 -3
View File
@@ -43,7 +43,7 @@ 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">'; 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) { for (const P of PIECES) {
if (P.hidden) continue; if (P.hidden) continue;
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 cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R cycles, F opens the menu)` : isWall(P.code) ? 'Square: 16 grains or 2 bars; slope: 8 grains or 1 bar' : costText(P.cost, 1);
const afford = P.kind === 'mold' || isWall(P.code) ? true : canAfford(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'; 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>`; 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() { function controlsHtml() {
const kb = [ 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'], ['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'], ['Sloped wall: turn · flip while dragging', 'T'], ['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'], ['Wall: cycle shape · flip diagonal drag', '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'], ['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'], ['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'], ['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'],
@@ -71,7 +71,7 @@ function controlsHtml() {
const tools = [ const tools = [
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'], ['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."], ['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'], ['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Right click takes a piece back. Your bot flies through anything you build.'],
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'], ['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'], ['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
]; ];
+12 -16
View File
@@ -101,6 +101,7 @@ function drawPreviews() {
} }
} else if (t === 'build') { } else if (t === 'build') {
const P = piece(), b = aimBlock(), code = pieceCode(P, b); const P = piece(), b = aimBlock(), code = pieceCode(P, b);
if (GAME.previewCode !== code) { GAME.previewCode = code; hudDirty = true; }
drawGrid(b.bx, b.by, 9); drawGrid(b.bx, b.by, 9);
let cells = [], col = '120,200,190'; let cells = [], col = '120,200,190';
if (P.kind === 'block') { if (P.kind === 'block') {
@@ -118,10 +119,10 @@ function drawPreviews() {
const ok = (!err || swap) && !ghost && afford; const ok = (!err || swap) && !ghost && afford;
const [x, y, w, h] = blockRectPx(bx, by, 1, 1); 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.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)';
previewBlock(code, x, y, w, h); previewBlock(code, x, y, w, h, false, P.id === 'tube' ? dIdx : -1);
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); 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]); if (ghost) ctx.setLineDash([zp, zp * 0.7]);
previewBlock(code, x, y, w, h, true); ctx.setLineDash([]); previewBlock(code, x, y, w, h, true, P.id === 'tube' ? dIdx : -1); ctx.setLineDash([]);
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) { 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) // a chevron on every block, pointing the way it will carry (or pump, or pour)
const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx]; const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
@@ -131,19 +132,6 @@ function drawPreviews() {
chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev(); chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev();
} }
} }
// dragging a line of belts, filters or tubes: one long arrow over it, the way it will run
if (GAME.drag && dd >= 0 && cells.length > 1 && !modShift()) {
const [x0, y0, w0, h0] = blockRectPx(cells[0][0], cells[0][1], 1, 1), [x1, y1, w1, h1] = blockRectPx(cells[cells.length - 1][0], cells[cells.length - 1][1], 1, 1);
let sx = x0 + w0 / 2, sy = y0 + h0 / 2, ex = x1 + w1 / 2, ey = y1 + h1 / 2;
const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dd] : [[-1, 0], [1, 0]][dd];
if ((ex - sx) * dx + (ey - sy) * dy < 0) { const tx = sx, ty = sy; sx = ex; sy = ey; ex = tx; ey = ty; }
const off = BS * zp * 1.0, hx = ex + dx * BS * zp * 0.55, hy = ey + dy * BS * zp * 0.55, ah = Math.max(8, BS * zp * 0.5);
const ox = dy ? off : 0, oy = dx ? off : 0;
const arrow = () => { ctx.beginPath(); ctx.moveTo(sx - dx * BS * zp * 0.3 + ox, sy - dy * BS * zp * 0.3 + oy); ctx.lineTo(hx + ox, hy + oy); ctx.moveTo(hx - dx * ah - dy * ah * 0.6 + ox, hy - dy * ah - dx * ah * 0.6 + oy); ctx.lineTo(hx + ox, hy + oy); ctx.lineTo(hx - dx * ah + dy * ah * 0.6 + ox, hy - dy * ah + dx * ah * 0.6 + oy); ctx.stroke(); };
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(20,16,12,0.6)'; ctx.lineWidth = Math.max(4, zp * 1.1); arrow();
ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(2, zp * 0.55); arrow();
}
if (GAME.drag && cells.length > 1) { if (GAME.drag && cells.length > 1) {
const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`; const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`;
ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)'; ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)';
@@ -191,8 +179,16 @@ function drawPreviews() {
drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2); 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) { function previewBlock(code, x, y, w, h, stroke = false, direction = -1) {
const ctx = R.ctx; const ctx = R.ctx;
if (code === BP_TUBE && direction >= 4) {
const rising = direction === 4 || direction === 6;
ctx.save(); ctx.translate(x + w / 2, y + h / 2); ctx.rotate(rising ? -Math.PI / 4 : Math.PI / 4);
const length = w * Math.SQRT2, width = w * 0.75;
if (stroke) ctx.strokeRect(-length / 2, -width / 2, length, width);
else ctx.fillRect(-length / 2, -width / 2, length, width);
ctx.restore(); return;
}
if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; } 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; const right = code === BP_SLOPE_BR || code === BP_SLOPE_TR, above = code >= BP_SLOPE_TL;
ctx.beginPath(); ctx.beginPath();
+11 -6
View File
@@ -91,17 +91,22 @@ 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 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) { function pieceCostNow(P, m = GAME.wallMat) {
if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls); if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls);
if (P.kind === 'block' && isWall(P.code)) return pieceCost(P.code, m); if (P.kind === 'block' && isWall(P.code)) return pieceCost(pieceCode(P), m);
return P.cost; return P.cost;
} }
function pieceCode(P, end = aimBlock()) { function pieceCode(P, end = aimBlock()) {
if (!isSlope(P.code)) return P.code; if (P.code !== BP_WALL) return P.code;
const a = GAME.drag || end; const a = GAME.drag || end, dx = end.bx - a.bx, dy = end.by - a.by;
return slopeCode(end.bx - a.bx, end.by - a.by, GAME.pieceDir.slope || 0); if (GAME.drag && (dx || dy)) {
return diagonalDrag(dx, dy) && !(typeof modShift === 'function' && modShift()) ? slopeCode(dx, dy, a.flip ? 2 : 0) : BP_WALL;
}
const shape = GAME.pieceDir.wall || 0;
return shape ? BP_SLOPE_BL + shape - 1 : BP_WALL;
} }
function turnSlope() { function turnSlope() {
const n = GAME.pieceDir.slope || 0; const a = GAME.drag, b = aimBlock();
GAME.pieceDir.slope = GAME.drag ? n ^ 2 : (n + 1) % 4; if (a && diagonalDrag(b.bx - a.bx, b.by - a.by)) a.flip = !a.flip;
else GAME.pieceDir.wall = ((GAME.pieceDir.wall || 0) + 1) % 5;
hudDirty = true; hudDirty = true;
} }
BIN
View File
Binary file not shown.
+14 -6
View File
@@ -677,14 +677,17 @@ with sync_playwright() as p:
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})')) check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
# Slopes use the same mouse drag path as walls, with T for their corner. # Slopes use the same mouse drag path as walls, with T for their corner.
pg.click('[data-piece="slope"]'); settle(60) pg.click('[data-piece="wall"]'); settle(60)
check('Sloped wall is next to Wall in the build menu', ev("piece().id === 'slope' && BUILD_CATS[0][1].slice(0,2).join(',') === 'wall,slope'")) check('Wall replaces the Sloped wall menu piece', ev("piece().id === 'wall' && !BUILD_ORDER.includes('slope') && !document.querySelector('[data-piece=\"slope\"]')"))
ev('GAME.wallMat = RSTONE; GAME.pieceDir.slope = 0') ev('GAME.wallMat = RSTONE; GAME.pieceDir.wall = 0')
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9)) drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('diagonal slope drag places four oriented blocks', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)')) check('diagonal slope drag places four oriented blocks', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
pg.keyboard.press('Control+z'); settle(60) pg.keyboard.press('Control+z'); settle(60)
pg.keyboard.press('t'); settle(60) pg.keyboard.press('t'); settle(60)
check('T cycles the single slope corner', ev('GAME.pieceDir.slope === 1')) check('T cycles the single slope corner', ev('GAME.pieceDir.wall === 1 && pieceCode(piece()) === BP_SLOPE_BL'))
for shape in [2, 3, 4, 0]:
pg.keyboard.press('t'); settle(40)
check(f'T cycles Wall shape {shape}', ev(f'GAME.pieceDir.wall === {shape} && pieceCode(piece()) === ' + ('BP_WALL' if shape == 0 else f'BP_SLOPE_BL+{shape}-1')))
pg.mouse.move(*blk(bbx - 5, bby - 12)); settle(60); pg.mouse.down() pg.mouse.move(*blk(bbx - 5, bby - 12)); settle(60); pg.mouse.down()
pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(60) pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(60)
pg.keyboard.press('t'); settle(60); pg.mouse.up(); settle(60) pg.keyboard.press('t'); settle(60); pg.mouse.up(); settle(60)
@@ -692,7 +695,7 @@ with sync_playwright() as p:
pg.keyboard.press('Control+z'); settle(60) pg.keyboard.press('Control+z'); settle(60)
pg.click('[data-piece="wall"]'); settle(60) pg.click('[data-piece="wall"]'); settle(60)
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9)) drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('square wall drag can also place a diagonal', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_WALL)')) check('Wall diagonal defaults back to solid below', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
pg.keyboard.press('Control+z'); settle(60) pg.keyboard.press('Control+z'); settle(60)
# Diagonal tubes use real mouse placement, preview arrows, R and picking. # Diagonal tubes use real mouse placement, preview arrows, R and picking.
@@ -732,12 +735,17 @@ with sync_playwright() as p:
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") ev("GAME.status = ''; GAME.statusT = 0")
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150) # Keep the camera still while aiming at a mold only four grains tall.
lead = ev('SET.cameraLead')
ev('SET.cameraLead = false; snapCamera()'); settle(100)
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 8, ev('molds[0].y') + 2)); settle(150)
check('mouse aims at the automatic ingot mold', ev('moldAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) === molds[0]'))
# count toasts made by the key press itself: counting the ones on screen also caught an older toast expiring # 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") ev("window._toastsMade = []; window._toastFn = toast; toast = function (t, o) { _toastsMade.push(t); return _toastFn(t, o); }; 0")
pg.keyboard.press('f'); settle(150) pg.keyboard.press('f'); settle(150)
made = ev("(() => { toast = _toastFn; return _toastsMade; })()") 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) check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and made == [], made)
ev(f'SET.cameraLead = {json.dumps(lead)}')
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 = '';
+14
View File
@@ -168,4 +168,18 @@ for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v); check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`)); check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
} }
// The drawing can bridge a joint only in unoccupied air; transport parts stay square.
run(fs.readFileSync('src/render/world.js', 'utf8'));
for (const v of [4,5,6,7]) {
flatWorld(run,240,192,176);
const rising = v===4 || v===6;
run(`placeBlock(BP_TUBE,${v},20,20,true);placeBlock(BP_TUBE,${v},21,${rising ? 19 : 21},true);`);
const x=84, y=rising ? 80 : 83;
check(`diagonal ${v} draws over the empty corner joint`,run(`diagonalTubeBlock(${x},${y},false)>=0 && part[${y}*W+${x}]===0`));
for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) {
check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalTubeBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`));
}
run(`removeBlock(21,${rising ? 19 : 21});`);
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalTubeBlock(${x},${y},false)===-1`));
}
done(); done();
+31 -4
View File
@@ -2,13 +2,19 @@
const { load, suite, flatWorld, rect, count, ticks } = require('./harness'); const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
const { run } = load(); const { run } = load();
const { check, done } = suite(); const { check, done } = suite();
// Wall selection drives the same saved slope codes and sealed funnel geometry.
const fs = require('fs');
run(fs.readFileSync('src/ui/selection.js', 'utf8'));
run(`const GAME = { pieceDir: {}, drag: null }; let hudDirty = false;
function aimBlock() { return { bx: 23, by: 23 }; }`);
function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); } function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); }
function funnel(sloped, material) { function funnel(sloped, material) {
fresh(); fresh();
run(`for (let k=0;k<12;k++) { run(`for (const [ax,ay,bx,by] of [[12,12,23,23],[36,12,25,23]]) {
placeBlock(${sloped ? 'BP_SLOPE_BL' : 'BP_WALL'}, RSTONE, 12+k, 12+k, true); GAME.drag={bx:ax,by:ay};
placeBlock(${sloped ? 'BP_SLOPE_BR' : 'BP_WALL'}, RSTONE, 36-k, 12+k, true); const code=${sloped ? 'pieceCode(PIECE_BY_ID.wall,{bx,by})' : 'BP_WALL'};
}`); for(const [x,y] of blockLine(ax,ay,bx,by,true)) placeBlock(code,RSTONE,x,y,true);
} GAME.drag=null;`);
rect(run, 80, 20, 119, 29, material); rect(run, 80, 20, 119, 29, material);
ticks(run, 1800); ticks(run, 1800);
const out = count(run, material, 0, 96, 239, 175); const out = count(run, material, 0, 96, 239, 175);
@@ -87,4 +93,25 @@ run('placeBlock(BP_SLOPE_BL,RSTONE,20,20,true);placeBlock(BP_WALL,RSTONE,20,21,t
check('removing an adjoining square wall restores the slope seal',run('mat[slopeSeal(BP_SLOPE_BL,20,20)]===STONE')); check('removing an adjoining square wall restores the slope seal',run('mat[slopeSeal(BP_SLOPE_BL,20,20)]===STONE'));
run('removeBlock(20,20)'); run('removeBlock(20,20)');
check('removal leaves no free seal grains behind',count(run,run('STONE'),80,80,83,87)===0); check('removal leaves no free seal grains behind',count(run,run('STONE'),80,80,83,87)===0);
check('old saved slope codes select Wall', run("Array.from({length:4},(_,k)=>PIECE_BY_CODE[15+k].id).every(id=>id==='wall')"));
check('single Wall starts square', run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
const shapes = [];
for (let k=0;k<5;k++) { run('turnSlope()'); shapes.push(run('pieceCode(PIECE_BY_ID.wall)')); }
check('T cycles square then four corners then square', JSON.stringify(shapes)===JSON.stringify([15,16,17,18,1]), shapes);
run('GAME.drag={bx:20,by:20}');
check('diagonal Wall defaults below', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_BL'));
run('turnSlope()');
check('T flips diagonal Wall above', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_TR'));
run('GAME.drag=null');
check('drag flip preserves single square choice',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
run('GAME.pieceDir.wall=3;GAME.drag={bx:20,by:23}');
check('straight Wall drag stays square even with a corner selected',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
run('GAME.drag=null;GAME.pieceDir.wall=1');
check('single Wall slope shows its half-block price',run('pieceCostNow(PIECE_BY_ID.wall,RSTONE)[0][1]===8'));
// Fixture saved before Wall absorbed the separate Sloped wall piece (save version 2).
const legacy = JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
globalThis.legacySlopeSave = legacy;
run('deserializeWorld(legacySlopeSave.meta,legacySlopeSave.buffers)');
check('old save loads all four slope corners unchanged',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10 && PIECE_BY_CODE[blockP[b]].id===\'wall\';}).every(Boolean)'));
delete globalThis.legacySlopeSave;
done(); done();
+7 -3
View File
@@ -7,10 +7,14 @@ for (let y = fy * BS - 45; y < floor + 8; y++) for (let x = fx * BS - 20; x < (f
setCell(i, y >= floor ? STONE : EMPTY, 20, y >= floor ? F_NAT : 0); setCell(i, y >= floor ? STONE : EMPTY, 20, y >= floor ? F_NAT : 0);
} }
for (let x = fx * BS - 20; x < (fx + 25) * BS + 20; x++) surfY[x] = floor; for (let x = fx * BS - 20; x < (fx + 25) * BS + 20; x++) surfY[x] = floor;
for (let k = 0; k < 12; k++) { bot.x = cx; bot.y = fy * BS + 24;
placeBlock(BP_SLOPE_BL, RBRICK, fx + k, fy + k, true); bagAdd(RBRICK, 192, true);
placeBlock(BP_SLOPE_BR, RBRICK, fx + 24 - k, fy + k, true); GAME.pieceDir.wall = 0;
for (const [ax, ay, bx, by] of [[fx, fy, fx + 11, fy + 11], [fx + 24, fy, fx + 13, fy + 11]]) {
GAME.drag = { bx: ax, by: ay };
placeBlocks(pieceCode(PIECE_BY_ID.wall, { bx, by }), RBRICK, blockLine(ax, ay, bx, by, true));
} }
GAME.drag = null;
for (let y = fy * BS - 28; y < fy * BS - 18; y++) for (let x = cx - 20; x < cx + 20; x++) setCell(y * W + x, SAND, 20, 0); for (let y = fy * BS - 28; y < fy * BS - 18; y++) for (let x = cx - 20; x < cx + 20; x++) setCell(y * W + x, SAND, 20, 0);
updateRoof(); updateRoof();
window.CAM = [cx, fy * BS + 25]; window.CAM = [cx, fy * BS + 25];