Build menu: Shift+Q/E switch tabs, G no longer picks (middle mouse does)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 17:12:51 -04:00
co-authored by Claude Opus 5.5
parent cab9343b26
commit 7d8df2ef67
10 changed files with 72 additions and 22 deletions
+4 -3
View File
@@ -10,10 +10,8 @@ and stay there until the owner has checked them in the game and deleted them.
- stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots? - stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots?
- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. - i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt.
- (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix. - (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix.
- for the build menu, add shift + q/e to move tabs.
- instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information. - instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information.
- dont need the short description in the build menu if we have a hover info popup. - dont need the short description in the build menu if we have a hover info popup.
- in the build menu, having the "g pick" there is confusing. im not sure if it should be there or not. also middle mouse the default for pick is better, g shouldnt even be used at all, its too far from the normal keys people use around wasd. maybe later for something less used.
- lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need. - lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need.
- the world still seems quite small. i dont know if thats because the bot moves so fast, the bot can mine too fast, or the grains should be smaller... i would like to make the worl seem huge to the player and give them loads of resources to mine for a long game. as is, you could mine all the resources from the map pretty quickly and end up having to start a new world and new game as a result. or maybe there is some other solution that i havent thought of yet. need to brainstorm this. - the world still seems quite small. i dont know if thats because the bot moves so fast, the bot can mine too fast, or the grains should be smaller... i would like to make the worl seem huge to the player and give them loads of resources to mine for a long game. as is, you could mine all the resources from the map pretty quickly and end up having to start a new world and new game as a result. or maybe there is some other solution that i havent thought of yet. need to brainstorm this.
@@ -25,4 +23,7 @@ and stay there until the owner has checked them in the game and deleted them.
## Done, please check ## Done, please check
(Nothing yet.) - for the build menu, add shift + q/e to move tabs.
- (agent) Shift+Q and Shift+E switch build-menu tabs, wrapping at the ends; each tab remembers the piece you last chose in it. Plain Q/E still cycle pieces. The keys are in the menu hint and the notebook's controls list.
- in the build menu, having the "g pick" there is confusing. im not sure if it should be there or not. also middle mouse the default for pick is better, g shouldnt even be used at all, its too far from the normal keys people use around wasd. maybe later for something less used.
- (agent) G does nothing now and is free for something later. Middle mouse picks, and the menu hint, build card and notebook controls list say so.
+4 -3
View File
@@ -79,7 +79,8 @@ Details and numbers live in the code and in `docs/systems/`.
- Machines: fan, Refiner, Stone cutter, Silo. - Machines: fan, Refiner, Stone cutter, Silo.
- Molds: ingot, bit, tank. - Molds: ingot, bit, tank.
- Build actions: drag for lines, Shift-drag for rectangles, Ctrl to replace, hold C / X and - 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:** - **Economy:**
- The trade dock pays for anything dropped through its grate, at the market price. - 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. - Prices dip when you sell a lot of one thing and recover over minutes.
@@ -135,8 +136,8 @@ above, including:
build more lines. build more lines.
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner - **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). reports this still misbehaves (see NOTES.md).
- **Pick** (middle mouse, also G for now) selects the piece under the cursor with its direction, - **Pick** (middle mouse) selects the piece under the cursor with its direction,
wall material and filter settings. The owner wants G dropped (see NOTES.md). 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, - **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). 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 - **No length limit when dragging.** Blocks out of the bot's reach become ghosts that it builds
+3 -3
View File
@@ -392,9 +392,9 @@ The notebook (N): milestones, market, wallet, blueprints, controls.
The tool hotbar and what is selected. The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, mixList, ensureDropMat, piece - 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. 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 - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey
### src/ui/selection.js (246 lines) ### 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. Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
### src/ui/input.js (76 lines) ### src/ui/input.js (75 lines)
Keyboard and mouse listeners. Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - variables: canvas
+17
View File
@@ -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 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-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) ## 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 Request: get the browser tests, Playwright and Chromium working on the owner's Arch machine
+1 -1
View File
@@ -45,7 +45,7 @@ function doInteract() {
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); } 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'); } 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. // direction, wall material and filter settings. On loose material it picks that to blow out.
function pickUnderCursor() { function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
+14 -6
View File
@@ -29,6 +29,17 @@ const PIECE_ICONS = {
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) { 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 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 = ''; 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() { function renderBuildBar() {
const bar = $('buildbar'); if (!bar) return; const bar = $('buildbar'); if (!bar) return;
const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld(); const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld();
@@ -42,7 +53,7 @@ function renderBuildBar() {
lastBuildKey = key; lastBuildKey = key;
const tip = PIECE_BY_ID[hov && ids.includes(hov) ? hov : P.id]; const tip = PIECE_BY_ID[hov && ids.includes(hov) ? hov : P.id];
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('')
+ '<span class="bb-keys"><kbd>Q</kbd>/<kbd>E</kbd> cycle · <kbd>G</kbd> pick</span></div><div class="bb-tiles">'; + '<span class="bb-keys"><kbd>Q</kbd>/<kbd>E</kbd> cycle · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab</span></div><div class="bb-tiles">';
for (const id of ids) { for (const id of ids) {
const Q = PIECE_BY_ID[id], cost = pieceCostNow(Q), ok = canAfford(cost, 1); const Q = PIECE_BY_ID[id], cost = pieceCostNow(Q), ok = canAfford(cost, 1);
const sw = Q.variants === 'wall' ? `<span class="sw" style="background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>` : ''; const sw = Q.variants === 'wall' ? `<span class="sw" style="background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>` : '';
@@ -57,11 +68,8 @@ function setupBuildBar() {
const bar = $('buildbar'); if (!bar) return; const bar = $('buildbar'); if (!bar) return;
bar.addEventListener('click', e => { bar.addEventListener('click', e => {
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return; const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
if (t.dataset.cat != null) { if (t.dataset.cat != null) { selectBuildCat(+t.dataset.cat); return; }
const c = +t.dataset.cat; GAME.buildCat = c; GAME.pieceIdx = PIECE_BY_ID[t.dataset.piece].index; GAME.buildCat = catOf(t.dataset.piece);
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); }
GAME.catPick = GAME.catPick || {}; GAME.catPick[catOf(piece().id)] = piece().id; GAME.catPick = GAME.catPick || {}; GAME.catPick[catOf(piece().id)] = piece().id;
GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui'); GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui');
}); });
+2 -3
View File
@@ -25,9 +25,8 @@ window.addEventListener('keydown', e => {
case '1': case '2': case '3': case '4': selectTool(+k - 1); break; case '1': case '2': case '3': case '4': selectTool(+k - 1); break;
case 'c': case 'x': hudDirty = true; break; case 'c': case 'x': hudDirty = true; break;
case 'v': startPaste(); break; case 'v': startPaste(); break;
case 'g': pickUnderCursor(); break; case 'q': if (e.shiftKey && toolId() === 'build') cycleBuildCat(-1); else cycleItem(-1); break;
case 'q': cycleItem(-1); break; case 'e': if (e.shiftKey && toolId() === 'build') cycleBuildCat(1); else cycleItem(1); break;
case 'e': cycleItem(1); break;
case 'r': rotate(); break; case 'r': rotate(); break;
case 't': if (toolId() === 'vacuum') toggleCollectUnderCursor(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break; case 't': if (toolId() === 'vacuum') toggleCollectUnderCursor(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
case 'f': doInteract(); break; case 'f': doInteract(); break;
+2 -2
View File
@@ -169,10 +169,10 @@ function copyCode(code) {
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'], ['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'], ['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'], ['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 = [ 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'], ['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
+1 -1
View File
@@ -199,7 +199,7 @@ function renderSelCard() {
<div class="sub">${esc(P.desc)}</div> <div class="sub">${esc(P.desc)}</div>
${P.filter ? `<div class="sub">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''} ${P.filter ? `<div class="sub">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
<div class="sub">${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 <kbd>R</kbd>.' : P.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(P) ? ' <kbd>R</kbd> picks the material.' : ''}</div> <div class="sub">${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 <kbd>R</kbd>.' : P.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(P) ? ' <kbd>R</kbd> picks the material.' : ''}</div>
<div class="sub keys-mini"><kbd>Ctrl</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · <kbd>G</kbd> pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>${ng ? `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</div>` : ''}`; <div class="sub keys-mini"><kbd>Ctrl</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>${ng ? `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</div>` : ''}`;
} else if (t === 'ignite') { } else if (t === 'ignite') {
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`; h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
} }
+24
View File
@@ -208,6 +208,30 @@ with sync_playwright() as p:
ok = select('3', "piece().id === 'wall'") ok = select('3', "piece().id === 'wall'")
ev('GAME.wallMat = RSTONE; hudDirty = true') ev('GAME.wallMat = RSTONE; hudDirty = true')
check('build tool starts on wall blocks', ok) 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)]') 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)') 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)) drag(lambda: blk(bbx + 3, bby - 4), lambda: blk(bbx + 8, bby - 4))