From f953a9cc512641d349f64f7c5e864005313b5da0 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Fri, 23 Oct 2026 20:16:49 -0400 Subject: [PATCH] Replace is held G instead of Ctrl (Ctrl+W/Ctrl+D were hit while flying) Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 8 +++++--- docs/PROJECT_MAP.md | 2 +- docs/agent-log.md | 15 +++++++++++++++ src/ui/actions.js | 10 +++++----- src/ui/build-menu.js | 6 +++--- src/ui/input.js | 12 ++++++------ src/ui/loop.js | 2 +- src/ui/notebook.js | 2 +- src/ui/previews.js | 6 +++--- src/ui/state.js | 2 +- test/play.py | 31 ++++++++++++++++++------------- 11 files changed, 59 insertions(+), 37 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 32bdb11..f33626a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -81,7 +81,7 @@ Details and numbers live in the code and in `docs/systems/`. leave one open block below and a belt under that carries them off. F is optional upkeep: it cleans the trough, chips off slag and rakes the furnace grate. Short stubs stay for the next firing. - - 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, hold G to replace, hold C / X and drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse to pick, Shift+Q/E to switch build tabs. - **Economy:** @@ -123,7 +123,7 @@ above, including: - **Copy and cut** are held keys, not a tool. Hold C or X and drag a box; it acts on release. Copy and cut look different. Cut is destructive, but it can be pasted back or undone. - **Ctrl+Z undoes building** (up to 80 steps). The bot building ghosts is never a step of its own. -- **Holding Ctrl** while placing replaces what is built there. +- **Holding Ctrl** while placing replaces what is built there (moved to G on 2026-10-23). - **Fans melt like copper (1085°C).** Put the fan outside the furnace, blowing in through a grate. The game warns when a fan gets close to melting. - **The vacuum takes only ticked materials** when anything is ticked, even if the drag starts on @@ -144,7 +144,7 @@ above, including: the load above it stands still and the belt slips underneath; the top no longer slides over a stuck bottom. Filters still let their chosen materials drop through. - **Pick** (middle mouse) selects the piece under the cursor with its direction, - wall material and filter settings. G is unbound, as the owner requested. + wall material and filter settings. G no longer picks, as the owner requested (it is now Replace). - **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving, Liquids, Machines, Molds. Hover a tile for its full description, cost and placement help; gamepad players see the selected tile’s popup. The top-left card is hidden while building, @@ -165,6 +165,8 @@ above, including: players will do it only when they need more ore rather than more money. - The project moved from a claude.ai artifact to local development with coding agents. It is built from small files, with `NOTES.md` as the owner's to-do inbox. +- **Replace is held G, not Ctrl.** The owner hit the browser's Ctrl+W (close tab) and Ctrl+D + (bookmark) while replacing and flying with WASD, and the game ignored WASD while Ctrl was held. ## Ideas not built yet diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 4fce5fd..9eeae9e 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -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 (75 lines) +### src/ui/input.js (79 lines) Keyboard and mouse listeners. - functions: isTyping - variables: canvas diff --git a/docs/agent-log.md b/docs/agent-log.md index e76a752..7611835 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,21 @@ 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: Replace is held G, not Ctrl (Claude Code, with Codex) + +Note: "i see the issue was ctrl+w closing the tab… it happens for the bookmark hotkey too. maybe +change the replace key?" The owner chose holding G. + +- `src/ui/input.js`: `GAME.ctrl` became `GAME.replace`, set while G is held. Ctrl no longer + does anything on its own, so WASD works while replacing. Ctrl+S, Ctrl+Z, Ctrl+wheel and the + leave-page prompt are unchanged. +- Flag users and text: `previews.js`, `actions.js` ("Hold G to replace it."), `build-menu.js` + (Replace · G chip, key hints), `loop.js`, `state.js`, `notebook.js` controls, `docs/DESIGN.md`. +- `test/play.py`: the replace checks hold G; a new check flies right with D while G is held + (121 checks pass). All 11 headless suites pass. `t_smooth.py`'s half-resolution check failed + on the slow fallback again (known, flaky under load). +- Open: nothing. + ## 2026-10-23: Ctrl+W no longer closes the game without asking (Claude Code) Note: "crash: hoplding crtl and pressing W crashes the whole browser." diff --git a/src/ui/actions.js b/src/ui/actions.js index 3669b66..3b2c9f9 100644 --- a/src/ui/actions.js +++ b/src/ui/actions.js @@ -137,15 +137,15 @@ function primaryPress(shift) { let err = ''; undoRecord(rec => piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-') ? `replacing with a ${P.name.toLowerCase()}` : `placing a ${P.name.toLowerCase()}`, () => { err = check() || ''; - // Ctrl: take down what you built in the way (refunded) and put this in its place - if (err && GAME.ctrl && /already/.test(err)) { + // G: take down what you built in the way (refunded) and put this in its place + if (err && GAME.replace && /already/.test(err)) { err = footprintBlocker(bx, by, bw, bh); if (!err && !canAfford(pieceCostNow(P), 1)) err = needText(pieceCostNow(P), 1); if (!err) err = clearFootprint(bx, by, bw, bh); } if (!err) err = place(); }); - if (err) { setStatus(err + (/already/.test(err) ? `${/[.!]$/.test(err) ? '' : '.'} Hold Ctrl to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; } + if (err) { setStatus(err + (/already/.test(err) ? `${/[.!]$/.test(err) ? '' : '.'} Hold G to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; } } } } @@ -175,7 +175,7 @@ function primaryRelease(shift) { const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by)); let ghosts = 0; const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => { - const res = placeBlocks(P.code, v, near, ex, GAME.ctrl); + const res = placeBlocks(P.code, v, near, ex, GAME.replace); const g = { t: 'paste', blocks: [], machines: [], molds: [] }; for (const [bx, by] of far) { const b = by * BW + bx; @@ -185,7 +185,7 @@ function primaryRelease(shift) { if (ghosts) undoNote(g); return res; }); - const hint = /already built/.test(r.status) ? `${/[.!]$/.test(r.status) ? '' : '.'} Hold Ctrl to replace it.` : ''; + const hint = /already built/.test(r.status) ? `${/[.!]$/.test(r.status) ? '' : '.'} Hold G to replace it.` : ''; const gtxt = ghosts ? `${r.n ? `Placed ${plural(r.n, P.name.toLowerCase())}; ` : ''}${plural(ghosts, 'more')} out of reach ${ghosts === 1 ? 'is a ghost' : 'are ghosts'} your bot builds when it gets close.` : ''; if (r.n || ghosts) { playSfx('place'); setStatus(gtxt || (r.status ? r.status + hint : ''), 'info'); } else { setStatus((r.status || 'Nothing placed.') + hint); playSfx('error'); } GAME.drag = null; GAME.dragDir = -1; hudDirty = true; diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js index 98e487c..1dec89c 100644 --- a/src/ui/build-menu.js +++ b/src/ui/build-menu.js @@ -50,7 +50,7 @@ function renderBuildBar() { const status = performance.now() - GAME.statusT < 3200 ? GAME.status : ''; const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length; 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.ctrl, GAME.pieceDir[P.id], !!GAME.drag, GAME.dragDir, GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), ng, ghostMissing, status, GAME.statusKind, innerWidth, innerHeight, SET.uiScale].join('|'); + const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.replace, GAME.pieceDir[P.id], !!GAME.drag, GAME.dragDir, GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), ng, ghostMissing, status, GAME.statusKind, innerWidth, innerHeight, SET.uiScale].join('|'); if (key === lastBuildKey) return; lastBuildKey = key; let h = '
' + BUILD_CATS.map(([name], k) => ``).join('') @@ -74,7 +74,7 @@ function renderBuildBar() { const variant = pieceUsesWall(P) ? `${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R` : P.filter ? `${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F${dirChip}` : dir ? dirChip : ''; - bar.querySelector('.bb-status').innerHTML = variant + (GAME.ctrl ? 'Replace · Ctrl' : '') + bar.querySelector('.bb-status').innerHTML = variant + (GAME.replace ? 'Replace · G' : '') + (ng ? `${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.` : '') + (status ? `${esc(status)}` : ''); const pop = $('bb-pop'); pop.hidden = !tip; @@ -83,7 +83,7 @@ function renderBuildBar() { pop.innerHTML = `
${pieceIcon(tip.id)}${esc(tip.name)}
${chipCost(pieceCostNow(tip))}${size}${tip.power ? `${tip.power} power` : ''}
${esc(tip.desc)}
How to place ${tip.kind === 'block' ? 'Drag for a line, Shift-drag for a rectangle; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses R.' : tip.dirs ? ' R turns it.' : pieceUsesWall(tip) ? ' R picks the material.' : ''}
${tip.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.
` : ''} -
Ctrl replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
`; +
hold G replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
`; const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; } const r = tile.getBoundingClientRect(), br = bar.getBoundingClientRect(), w = pop.getBoundingClientRect().width; pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`; diff --git a/src/ui/input.js b/src/ui/input.js index 4cd9564..7ed039f 100644 --- a/src/ui/input.js +++ b/src/ui/input.js @@ -11,7 +11,6 @@ window.addEventListener('keydown', e => { return; } if (!GAME.running) return; - if (e.key === 'Control') { GAME.ctrl = true; return; } if (e.ctrlKey || e.metaKey) { if (k === 's') { e.preventDefault(); saveWorld('main'); } else if (k === 'z' && !e.shiftKey) { e.preventDefault(); doUndo(); } @@ -30,6 +29,7 @@ window.addEventListener('keydown', e => { case 'r': rotate(); break; case 't': if (toolId() === 'vacuum') toggleCollectUnderCursor(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break; case 'f': doInteract(); break; + case 'g': GAME.replace = true; hudDirty = true; break; case 'h': toggleLens(); break; case 'i': GAME.infoOverlay = !GAME.infoOverlay; playSfx('ui'); toast(GAME.infoOverlay ? 'Info overlay on: what every filter does, and which way belts and tubes run. I hides it.' : 'Info overlay off.'); break; case 'l': bot.lamp = !bot.lamp; playSfx('ui'); break; @@ -43,9 +43,9 @@ window.addEventListener('keydown', e => { case 'Tab': $('bagpanel').hidden = !$('bagpanel').hidden; break; } }); -window.addEventListener('keyup', e => { const k = e.key.length === 1 ? e.key.toLowerCase() : e.key; keys.delete(k); if (e.key === 'Control') GAME.ctrl = false; if (k === 'c' || k === 'x') hudDirty = true; }); -window.addEventListener('blur', () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.ctrl = false; GAME.selDrag = null; endRmbRecord(); }); -canvas.addEventListener('mousemove', e => { GAME.ctrl = e.ctrlKey; GAME.mouse.x = e.clientX; GAME.mouse.y = e.clientY; GAME.mouse.in = true; if (GAME.usingPad) { GAME.usingPad = false; canvas.classList.remove('nocursor'); } }); +window.addEventListener('keyup', e => { const k = e.key.length === 1 ? e.key.toLowerCase() : e.key; keys.delete(k); if (k === 'g') { GAME.replace = false; hudDirty = true; } if (k === 'c' || k === 'x') hudDirty = true; }); +window.addEventListener('blur', () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.replace = false; GAME.selDrag = null; endRmbRecord(); }); +canvas.addEventListener('mousemove', e => { GAME.mouse.x = e.clientX; GAME.mouse.y = e.clientY; GAME.mouse.in = true; if (GAME.usingPad) { GAME.usingPad = false; canvas.classList.remove('nocursor'); } }); canvas.addEventListener('mouseleave', () => { GAME.mouse.in = false; }); canvas.addEventListener('contextmenu', e => e.preventDefault()); canvas.addEventListener('mousedown', e => { @@ -57,7 +57,7 @@ canvas.addEventListener('mousedown', e => { else if (e.button === 1) { e.preventDefault(); pickUnderCursor(); } }); window.addEventListener('mouseup', e => { - if (e.button === 0) { GAME.ctrl = e.ctrlKey; if (GAME.lmb) primaryRelease(modShift()); GAME.lmb = false; } + if (e.button === 0) { if (GAME.lmb) primaryRelease(modShift()); GAME.lmb = false; } else if (e.button === 2) { GAME.rmb = false; endRmbRecord(); } }); canvas.addEventListener('wheel', e => { @@ -74,6 +74,6 @@ document.addEventListener('visibilitychange', () => { if (document.hidden) { if (SET.muteUnfocused) AU.ctx.suspend(); } else AU.ctx.resume(); }); -// Ctrl+W closes the tab and the page cannot stop it, and Ctrl (replace) + W (fly) is an easy slip: +// Ctrl+W closes the tab and the page cannot stop it: // while a world is open, ask the browser to confirm leaving window.addEventListener('beforeunload', e => { if (GAME.running) { e.preventDefault(); e.returnValue = ''; } }); diff --git a/src/ui/loop.js b/src/ui/loop.js index abbe7f9..4cb2153 100644 --- a/src/ui/loop.js +++ b/src/ui/loop.js @@ -74,7 +74,7 @@ function frame(now) { drawRain(); drawBot(); if (!GAME.hidePreviews) drawPreviews(); // tools/shot.py hides them for clean screenshots - if (GAME.ctrl !== GAME.ctrlShown) { GAME.ctrlShown = GAME.ctrl; hudDirty = true; } + if (GAME.replace !== GAME.replaceShown) { GAME.replaceShown = GAME.replace; hudDirty = true; } renderBuildBar(); if (hudDirty || R.frame % 6 === 0) { renderSelCard(); renderBag(); renderWallet(); hudDirty = false; } if (R.frame % 4 === 0) renderInfo(); diff --git a/src/ui/notebook.js b/src/ui/notebook.js index eaf4df1..c6442fe 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -171,7 +171,7 @@ function controlsHtml() { ['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'], ['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'], + ['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', 'M'], ]; const pad = [ diff --git a/src/ui/previews.js b/src/ui/previews.js index 1ef070d..9c8a18a 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -176,7 +176,7 @@ function drawPreviews() { const afford = canAfford(pieceCostNow(P), cells.length); for (const [bx, by] of cells) { const err = canPlaceBlock(bx, by), rb = by * BW + bx; - const swap = GAME.ctrl && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, P.code, v, pieceExtra(P)); + const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, P.code, v, pieceExtra(P)); const ghost = !blockInReach(bx, by) && (!err || /bar/.test(err)); // out of reach: it will be a ghost const ok = (!err || swap) && !ghost && afford; const [x, y, w, h] = blockRectPx(bx, by, 1, 1); @@ -217,7 +217,7 @@ function drawPreviews() { const bw = P.kind === 'mold' ? MOLDS[P.mold].bw : P.bw, bh = P.kind === 'mold' ? MOLDS[P.mold].bh : P.bh; const { bx, by } = stampOrigin(bw, bh); const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by); - const swap = !!err && GAME.ctrl && /already/.test(err) && !footprintBlocker(bx, by, bw, bh); + const swap = !!err && GAME.replace && /already/.test(err) && !footprintBlocker(bx, by, bw, bh); const ok = (!err || swap) && inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2) && canAfford(pieceCostNow(P), 1); const [x, y, w, h] = blockRectPx(bx, by, bw, bh); if (swap && ok) { ctx.fillStyle = 'rgba(255,190,90,0.35)'; ctx.fillRect(x, y, w, h); } @@ -250,7 +250,7 @@ function drawPreviews() { ctx.beginPath(); ctx.moveTo(cx - 10, cy); ctx.lineTo(cx - 4, cy); ctx.moveTo(cx + 4, cy); ctx.lineTo(cx + 10, cy); ctx.moveTo(cx, cy - 10); ctx.lineTo(cx, cy - 4); ctx.moveTo(cx, cy + 4); ctx.lineTo(cx, cy + 10); ctx.stroke(); } ctx.restore(); - if (t === 'build' && GAME.ctrl && !sel && !pasting) drawTag('Replace', '255,190,90', p.x + BS * zp * 1.2, p.y + BS * zp * 1.4); + if (t === 'build' && GAME.replace && !sel && !pasting) drawTag('Replace', '255,190,90', p.x + BS * zp * 1.2, p.y + BS * zp * 1.4); drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2); } // a small labelled tag drawn on the world (screen pixels) diff --git a/src/ui/state.js b/src/ui/state.js index b366967..0bec709 100644 --- a/src/ui/state.js +++ b/src/ui/state.js @@ -40,7 +40,7 @@ function loadCollect() { try { const a = JSON.parse(localStorage.getItem('deepgr const GAME = { running: false, paused: false, tool: 0, dropMat: 0, matChosen: false, wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {}, brush: [3, 2, 0, 0, 0], collect: loadCollect(), tipsOpen: new Set(), cards: [], vacFilter: 0, vacLocked: false, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 }, - paste: false, selDrag: null, rmbRec: null, dragDir: -1, ctrlShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false }, + paste: false, selDrag: null, rmbRec: null, dragDir: -1, replaceShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false }, lmb: false, rmb: false, blowMix: false, drillW: 1, drillTune: 0, lastDug: 0, padRun: 0, status: '', statusKind: 'err', statusT: 0, usingPad: false, padAim: { x: 12, y: 0 }, lastAutosave: 0, playTime: 0, lastHud: 0, worldName: '', }; diff --git a/test/play.py b/test/play.py index a04bbc2..a3b5b6f 100644 --- a/test/play.py +++ b/test/play.py @@ -27,7 +27,7 @@ with sync_playwright() as p: ev = pg.evaluate look = sys.argv[2] if len(sys.argv) > 2 else 'pixel' check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom')) - # Ctrl+W (Ctrl to replace, W to fly) closes the tab; with a world open the browser must ask first + # Ctrl+W closes the tab; with a world open the browser must ask first leave = "(() => { const e = new Event('beforeunload', { cancelable: true }); dispatchEvent(e); return e.defaultPrevented; })()" check('leaving the page with a world open asks to confirm', ev(leave)) check('but not on the title screen', not ev(f'(() => {{ GAME.running = false; const r = {leave}; GAME.running = true; return r; }})()')) @@ -339,16 +339,21 @@ with sync_playwright() as p: pg.keyboard.press('Escape'); settle(100) pg.keyboard.press('Control+z'); settle(300) check('Ctrl+Z puts the cut row back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and ev('money') == m_before, ev('[money, GAME.status]')) - # Ctrl with a machine: it replaces the belt it is placed on + # G with a machine: it replaces the belt it is placed on ok = select('3', "piece().id === 'fan'") pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400) pg.mouse.down(); pg.mouse.up(); settle(100) - check('a fan on a belt says to hold Ctrl', 'Ctrl' in ev('GAME.status'), ev('GAME.status')) - pg.keyboard.down('Control'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200) - check('holding Ctrl shows replace mode', 'Replace' in ev("$('buildbar').textContent")) + check('a fan on a belt says to hold G', 'Hold G' in ev('GAME.status'), ev('GAME.status')) + pg.keyboard.down('g'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200) + check('holding G shows replace mode', 'Replace' in ev("$('buildbar').textContent")) pg.screenshot(path=str(out / '07g-replace-fan.png')) - pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('Control'); settle(100) - check('Ctrl-click puts the fan in place of the belt', ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4 && m.y === ({bby} + 1) * 4)") and ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0, ev('GAME.status')) + pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('g'); settle(100) + check('G-click puts the fan in place of the belt', ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4 && m.y === ({bby} + 1) * 4)") and ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0, ev('GAME.status')) + x_before, y_before = ev('[bot.x, bot.y]') + pg.keyboard.down('g'); pg.keyboard.down('d'); settle(300) + pg.keyboard.up('d'); pg.keyboard.up('g') + check('flying with WASD works while G (replace) is held', ev('bot.x') > x_before, ev('bot.x') - x_before) + ev(f'bot.x = {x_before}; bot.y = {y_before}; R.snapNext = true;'); settle(200) # pipes are laid by dragging, like belts ok = select('3', "piece().id === 'pipe'") check('pipes, pumps, outlets and the refiner are in the build menu', ok and all(x in ev("PIECES.filter(p => !p.hidden).map(p => p.id)") for x in ['pipe', 'pump', 'outlet', 'refiner'])) @@ -360,7 +365,7 @@ with sync_playwright() as p: pg.keyboard.press('Control+z'); settle(200) check('and Ctrl+Z swaps the belt back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and not ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4)"), ev('GAME.status')) - # 7b. filter: choose several materials with F, place a line, replace belts with Ctrl, edit the whole line + # 7b. filter: choose several materials with F, place a line, replace belts with G, edit the whole line ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };') ok = select('3', "piece().id === 'filter'") check('select filter', ok) @@ -375,16 +380,16 @@ with sync_playwright() as p: drag(lambda: blk(bbx - 6, fy), lambda: blk(bbx - 3, fy)) nf = ev('blockP.reduce((a, v) => a + (v === BP_FILTER), 0)') check('filters placed by drag, dropping both ores and carrying right', nf == 4 and ev(f'blockV[{fy}*BW+{bbx-5}]') == 128 and ev(f'filterInfo({fy}*BW+{bbx-5}).m.length') == 2, f"{nf} {ev(f'JSON.stringify(filterInfo({fy}*BW+{bbx-5}))')}") - # two belts at the end of the line: plain dragging leaves them, Ctrl-dragging swaps them for filters + # two belts at the end of the line: plain dragging leaves them, G-dragging swaps them for filters ev(f"placeBlock(BP_BELT, 1, {bbx-2}, {fy}, true); placeBlock(BP_BELT, 1, {bbx-1}, {fy}, true);") drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy)) - check('dragging over built belts leaves them and says how to replace', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_BELT') and 'Ctrl' in ev('GAME.status'), ev('GAME.status')) - pg.keyboard.down('Control'); settle(50) + check('dragging over built belts leaves them and says how to replace', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_BELT') and 'Hold G' in ev('GAME.status'), ev('GAME.status')) + pg.keyboard.down('g'); settle(50) pg.mouse.move(*blk(bbx - 2, fy)); settle(300) pg.screenshot(path=str(out / '07c-replace-preview.png')) drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy)) - pg.keyboard.up('Control'); settle(50) - check('Ctrl-dragging replaces them with filters', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_FILTER && blockP[{fy}*BW+{bbx-1}] === BP_FILTER') and ev(f'filterRun({fy}*BW+{bbx-5}).length') == 6, ev('GAME.status')) + pg.keyboard.up('g'); settle(50) + check('G-dragging replaces them with filters', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_FILTER && blockP[{fy}*BW+{bbx-1}] === BP_FILTER') and ev(f'filterRun({fy}*BW+{bbx-5}).length') == 6, ev('GAME.status')) pg.mouse.move(*blk(bbx - 5, fy)); settle(200) check('hovering a filter says what it drops and carries', 'copper ore' in ev('JSON.stringify(hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)))').lower()) ev(f"for (let y = {fy*4 - 10}; y < {fy*4 - 1}; y++) for (let x = {(bbx-6)*4}; x < {(bbx-5)*4 + 2}; x++) setCell(y*W+x, [SAND, CUORE, FEORE][(x+y)%3], 20, 0);")