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