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 = '
' + BUILD_CATS.map(([name], k) => ``).join('') - + 'Q/E cycle · G pick
'; + + 'Q/E cycle · Shift+Q/E tab
'; 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))