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 ce7df82b01
commit 6676d99c66
9 changed files with 68 additions and 19 deletions
+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))