diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index d77a568..16ad374 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -79,7 +79,8 @@ Details and numbers live in the code and in `docs/systems/`.
- Machines: fan, Refiner, Stone cutter, Silo.
- Molds: ingot, bit, tank.
- Build actions: drag for lines, Shift-drag for rectangles, Ctrl to replace, hold C / X and
- drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse (or G) to pick.
+ drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse to pick,
+ Shift+Q/E to switch build tabs.
- **Economy:**
- The trade dock pays for anything dropped through its grate, at the market price.
- Prices dip when you sell a lot of one thing and recover over minutes.
@@ -135,8 +136,8 @@ above, including:
build more lines.
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner
reports this still misbehaves (see NOTES.md).
-- **Pick** (middle mouse, also G for now) selects the piece under the cursor with its direction,
- wall material and filter settings. The owner wants G dropped (see NOTES.md).
+- **Pick** (middle mouse) selects the piece under the cursor with its direction,
+ wall material and filter settings. G is unbound, as the owner requested.
- **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving,
Liquids, Machines, Molds. The owner wants more changes (see NOTES.md).
- **No length limit when dragging.** Blocks out of the bot's reach become ghosts that it builds
diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md
index 4f5f537..48c9271 100644
--- a/docs/PROJECT_MAP.md
+++ b/docs/PROJECT_MAP.md
@@ -392,9 +392,9 @@ The notebook (N): milestones, market, wallet, blueprints, controls.
The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, mixList, ensureDropMat, piece
-### src/ui/build-menu.js (72 lines)
+### src/ui/build-menu.js (80 lines)
The build menu along the bottom: categories, piece tiles and their icons.
-- functions: pieceIcon, catOf, renderBuildBar, setupBuildBar
+- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey
### src/ui/selection.js (246 lines)
@@ -406,7 +406,7 @@ The selected tool and piece: R / Q / E, drill width, filter picker, the selectio
Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
-### src/ui/input.js (76 lines)
+### src/ui/input.js (75 lines)
Keyboard and mouse listeners.
- functions: isTyping
- variables: canvas
diff --git a/docs/agent-log.md b/docs/agent-log.md
index ea4165e..1467911 100644
--- a/docs/agent-log.md
+++ b/docs/agent-log.md
@@ -4,6 +4,23 @@ 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-23: Shift+Q/E build tabs, G pick key removed (Claude Code, with Codex)
+
+Notes: "add shift + q/e to move tabs" and "having the "g pick" there is confusing".
+
+- `src/ui/build-menu.js`: `selectBuildCat(c)` (shared with tab clicks) and `cycleBuildCat(d)`;
+ the menu hint now reads Q/E cycle · Shift+Q/E tab.
+- `src/ui/input.js`: Shift+Q/E switch tabs with the build tool; G is unbound.
+- Text: build card (`selection.js`), notebook controls (`notebook.js`), `actions.js` comment,
+ `docs/DESIGN.md`, `docs/PROJECT_MAP.md` (regenerated, `--check` passes).
+- `test/play.py`: checks for Shift+Q/E, wrapping, the remembered piece per tab, plain Q/E,
+ and that G no longer calls pick while middle mouse still does (109 checks).
+- Tests: all 11 headless suites and `play.py` pass; a `tools/shot.py` shot of the Moving tab
+ shows the new hint. `t_smooth.py` passes, but its "half-resolution smooth still draws" check
+ failed on several runs with the 'slow' pixel fallback while the machine was busy (load ~14).
+ The previous commit fails the same way under the same load, so this change didn't cause it.
+- Open: that check is timing-sensitive on a loaded machine; rerun it when the machine is idle.
+
## 2026-10-23: browser tests run locally (Claude Code, with Codex)
Request: get the browser tests, Playwright and Chromium working on the owner's Arch machine
diff --git a/src/ui/actions.js b/src/ui/actions.js
index ee7aabf..3669b66 100644
--- a/src/ui/actions.js
+++ b/src/ui/actions.js
@@ -45,7 +45,7 @@ function doInteract() {
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
else { setStatus('Nothing to use there. F works on gates, molds, fans and filters.', 'info'); }
}
-// Pick (G or middle mouse): the piece under the cursor becomes the one you build, with its
+// Pick (middle mouse): the piece under the cursor becomes the one you build, with its
// direction, wall material and filter settings. On loose material it picks that to blow out.
function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js
index 6100ddf..7977cb0 100644
--- a/src/ui/build-menu.js
+++ b/src/ui/build-menu.js
@@ -29,6 +29,17 @@ const PIECE_ICONS = {
function pieceIcon(id) { return ``; }
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
let lastBuildKey = '';
+function selectBuildCat(c) {
+ GAME.buildCat = c;
+ const ids = BUILD_CATS[c][1], keep = GAME.catPick && GAME.catPick[c];
+ GAME.pieceIdx = PIECE_BY_ID[ids.includes(keep) ? keep : ids[0]].index;
+ GAME.catPick = GAME.catPick || {}; GAME.catPick[c] = piece().id;
+ GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui');
+}
+function cycleBuildCat(d) {
+ const c = GAME.buildCat != null ? GAME.buildCat : catOf(piece().id);
+ selectBuildCat((c + d + BUILD_CATS.length) % BUILD_CATS.length);
+}
function renderBuildBar() {
const bar = $('buildbar'); if (!bar) return;
const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld();
@@ -42,7 +53,7 @@ function renderBuildBar() {
lastBuildKey = key;
const tip = PIECE_BY_ID[hov && ids.includes(hov) ? hov : P.id];
let h = '
';
for (const id of ids) {
const Q = PIECE_BY_ID[id], cost = pieceCostNow(Q), ok = canAfford(cost, 1);
const sw = Q.variants === 'wall' ? `` : '';
@@ -57,11 +68,8 @@ function setupBuildBar() {
const bar = $('buildbar'); if (!bar) return;
bar.addEventListener('click', e => {
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
- if (t.dataset.cat != null) {
- const c = +t.dataset.cat; GAME.buildCat = c;
- const ids = BUILD_CATS[c][1], keep = GAME.catPick && GAME.catPick[c];
- GAME.pieceIdx = PIECE_BY_ID[ids.includes(keep) ? keep : ids[0]].index;
- } else { GAME.pieceIdx = PIECE_BY_ID[t.dataset.piece].index; GAME.buildCat = catOf(t.dataset.piece); }
+ if (t.dataset.cat != null) { selectBuildCat(+t.dataset.cat); return; }
+ GAME.pieceIdx = PIECE_BY_ID[t.dataset.piece].index; GAME.buildCat = catOf(t.dataset.piece);
GAME.catPick = GAME.catPick || {}; GAME.catPick[catOf(piece().id)] = piece().id;
GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui');
});
diff --git a/src/ui/input.js b/src/ui/input.js
index f87d184..659bc68 100644
--- a/src/ui/input.js
+++ b/src/ui/input.js
@@ -25,9 +25,8 @@ window.addEventListener('keydown', e => {
case '1': case '2': case '3': case '4': selectTool(+k - 1); break;
case 'c': case 'x': hudDirty = true; break;
case 'v': startPaste(); break;
- case 'g': pickUnderCursor(); break;
- case 'q': cycleItem(-1); break;
- case 'e': 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 'r': rotate(); break;
case 't': if (toolId() === 'vacuum') toggleCollectUnderCursor(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
case 'f': doInteract(); break;
diff --git a/src/ui/notebook.js b/src/ui/notebook.js
index ca4fec8..40b82fb 100644
--- a/src/ui/notebook.js
+++ b/src/ui/notebook.js
@@ -169,10 +169,10 @@ function copyCode(code) {
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'], ['Cycle what you blow · piece', 'Q / E'], ['Variant: material · direction · flip', 'R'],
+ ['Tools', '1 – 4'], ['Cycle what you blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'],
['Tick/untick what is under the cursor (Vacuum)', 'R or T'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
['Replace what is built (Build)', 'Hold Ctrl'], ['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)', 'G or middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'M'],
+ ['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', 'M'],
];
const pad = [
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
diff --git a/src/ui/selection.js b/src/ui/selection.js
index beb7c03..8100185 100644
--- a/src/ui/selection.js
+++ b/src/ui/selection.js
@@ -199,7 +199,7 @@ function renderSelCard() {
${esc(P.desc)}
${P.filter ? `
New ones will ${esc(filterSentence(GAME.filterCfg))}. F sets materials and mode (or click materials in the bag, or T over one); F on a placed filter changes its whole row.
` : ''}
${P.kind === 'block' ? 'Drag for a line (any length: what is out of reach becomes ghosts), Shift-drag for a rectangle.' : 'Click to place.'}${P.dirs && P.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses R.' : P.dirs ? ' R turns it.' : pieceUsesWall(P) ? ' R picks the material.' : ''}
-
Ctrl replace · Ctrl+Z undo · right click take back · G pick · hold C/X copy/cut · V paste
${ng ? `
${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.
` : ''}`;
+
Ctrl replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
${ng ? `
${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.
` : ''}`;
} else if (t === 'ignite') {
h = `
Igniterup to 640°C
Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.
`;
}
diff --git a/test/play.py b/test/play.py
index 1d09d37..4bfa6ce 100644
--- a/test/play.py
+++ b/test/play.py
@@ -208,6 +208,30 @@ with sync_playwright() as p:
ok = select('3', "piece().id === 'wall'")
ev('GAME.wallMat = RSTONE; hudDirty = true')
check('build tool starts on wall blocks', ok)
+ c0 = ev('GAME.buildCat != null ? GAME.buildCat : catOf(piece().id)')
+ pg.keyboard.press('Shift+E')
+ check('Shift+E switches build tabs and selects a piece in the tab', ev('GAME.buildCat') == (c0 + 1) % ev('BUILD_CATS.length') and ev('BUILD_CATS[GAME.buildCat][1].includes(piece().id)'))
+ pg.keyboard.press('Shift+Q')
+ check('Shift+Q returns to the tab and restores its wall piece', ev('GAME.buildCat') == c0 and ev("piece().id === 'wall'"))
+ ev('selectBuildCat(0)')
+ pg.keyboard.press('Shift+Q')
+ check('Shift+Q wraps from the first build tab to the last', ev('GAME.buildCat === BUILD_CATS.length - 1'))
+ pg.keyboard.press('Shift+E')
+ check('Shift+E wraps back to the first build tab', ev('GAME.buildCat === 0'))
+ p0 = ev('piece().id')
+ pg.keyboard.press('e')
+ check('plain E cycles pieces without switching tabs', ev('piece().id') != p0 and ev('GAME.buildCat') == 0)
+ pg.keyboard.press('q')
+ check('plain Q returns to the piece without switching tabs', ev('piece().id') == p0 and ev('GAME.buildCat') == 0)
+ pg.mouse.move(*cell_to_screen(bx + 6, gy + 8)); settle(100)
+ ev('window._pickN = 0; window._pickOrig = pickUnderCursor; pickUnderCursor = function () { _pickN++; }; 0') # 0: evaluate would call a returned function
+ pg.keyboard.press('g'); settle(60)
+ g_picks = ev('_pickN')
+ pg.mouse.click(*cell_to_screen(bx + 6, gy + 8), button='middle'); settle(60)
+ check('G no longer picks; middle mouse still does', g_picks == 0 and ev('_pickN') == 1, f"G {g_picks}, then {ev('_pickN')}")
+ ev('pickUnderCursor = _pickOrig')
+ check('G leaves the build piece unchanged over the world', ev('piece().id') == p0)
+ ev("GAME.pieceIdx = PIECE_BY_ID.wall.index; GAME.wallMat = RSTONE; hudDirty = true")
bbx, bby = ev('[Math.floor((bot.x + 2) / 4), Math.floor((bot.y + 2) / 4)]')
rb = ev('bag[RSTONE]'); nb0 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
drag(lambda: blk(bbx + 3, bby - 4), lambda: blk(bbx + 8, bby - 4))