Replace is held G instead of Ctrl (Ctrl+W/Ctrl+D were hit while flying)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:16:59 -04:00
co-authored by Claude Opus 5.5
parent 0679878e69
commit da81de1b6c
11 changed files with 59 additions and 37 deletions
+5 -3
View File
@@ -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 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 cleans the trough, chips off slag and rakes the furnace grate. Short stubs stay for the next
firing. 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, drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse to pick,
Shift+Q/E to switch build tabs. Shift+Q/E to switch build tabs.
- **Economy:** - **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** 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. 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. - **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. - **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 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 - **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 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. a stuck bottom. Filters still let their chosen materials drop through.
- **Pick** (middle mouse) 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. 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, - **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; 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, 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. 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 - 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. 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 ## Ideas not built yet
+1 -1
View File
@@ -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 (75 lines) ### src/ui/input.js (79 lines)
Keyboard and mouse listeners. Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - variables: canvas
+15
View File
@@ -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 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: 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) ## 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." Note: "crash: hoplding crtl and pressing W crashes the whole browser."
+5 -5
View File
@@ -137,15 +137,15 @@ function primaryPress(shift) {
let err = ''; let err = '';
undoRecord(rec => piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-') ? `replacing with a ${P.name.toLowerCase()}` : `placing a ${P.name.toLowerCase()}`, () => { undoRecord(rec => piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-') ? `replacing with a ${P.name.toLowerCase()}` : `placing a ${P.name.toLowerCase()}`, () => {
err = check() || ''; err = check() || '';
// Ctrl: take down what you built in the way (refunded) and put this in its place // G: take down what you built in the way (refunded) and put this in its place
if (err && GAME.ctrl && /already/.test(err)) { if (err && GAME.replace && /already/.test(err)) {
err = footprintBlocker(bx, by, bw, bh); err = footprintBlocker(bx, by, bw, bh);
if (!err && !canAfford(pieceCostNow(P), 1)) err = needText(pieceCostNow(P), 1); if (!err && !canAfford(pieceCostNow(P), 1)) err = needText(pieceCostNow(P), 1);
if (!err) err = clearFootprint(bx, by, bw, bh); if (!err) err = clearFootprint(bx, by, bw, bh);
} }
if (!err) err = place(); 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)); const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
let ghosts = 0; let ghosts = 0;
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => { 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: [] }; const g = { t: 'paste', blocks: [], machines: [], molds: [] };
for (const [bx, by] of far) { for (const [bx, by] of far) {
const b = by * BW + bx; const b = by * BW + bx;
@@ -185,7 +185,7 @@ function primaryRelease(shift) {
if (ghosts) undoNote(g); if (ghosts) undoNote(g);
return res; 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.` : ''; 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'); } 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; GAME.drag = null; GAME.dragDir = -1; hudDirty = true;
+3 -3
View File
@@ -50,7 +50,7 @@ function renderBuildBar() {
const status = performance.now() - GAME.statusT < 3200 ? GAME.status : ''; const status = performance.now() - GAME.statusT < 3200 ? GAME.status : '';
const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length; const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length;
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join(''); 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; if (key === lastBuildKey) return;
lastBuildKey = key; lastBuildKey = key;
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('')
@@ -74,7 +74,7 @@ function renderBuildBar() {
const variant = pieceUsesWall(P) ? `<span class="chip hot"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R</span>` const variant = pieceUsesWall(P) ? `<span class="chip hot"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R</span>`
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}` : P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
: dir ? dirChip : ''; : dir ? dirChip : '';
bar.querySelector('.bb-status').innerHTML = variant + (GAME.ctrl ? '<span class="chip hot">Replace · Ctrl</span>' : '') bar.querySelector('.bb-status').innerHTML = variant + (GAME.replace ? '<span class="chip hot">Replace · G</span>' : '')
+ (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '') + (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '')
+ (status ? `<span class="chip ${GAME.statusKind === 'info' ? 'ok' : 'bad'}">${esc(status)}</span>` : ''); + (status ? `<span class="chip ${GAME.statusKind === 'info' ? 'ok' : 'bad'}">${esc(status)}</span>` : '');
const pop = $('bb-pop'); pop.hidden = !tip; const pop = $('bb-pop'); pop.hidden = !tip;
@@ -83,7 +83,7 @@ function renderBuildBar() {
pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div> pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div>
<div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${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 <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> picks the material.' : ''}</div> <div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${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 <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> picks the material.' : ''}</div>
${tip.filter ? `<div class="bb-place">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>` : ''} ${tip.filter ? `<div class="bb-place">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="bb-place"><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>`; <div class="bb-place">hold <kbd>G</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>`;
const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; } 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; 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`; pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`;
+6 -6
View File
@@ -11,7 +11,6 @@ window.addEventListener('keydown', e => {
return; return;
} }
if (!GAME.running) return; if (!GAME.running) return;
if (e.key === 'Control') { GAME.ctrl = true; return; }
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
if (k === 's') { e.preventDefault(); saveWorld('main'); } if (k === 's') { e.preventDefault(); saveWorld('main'); }
else if (k === 'z' && !e.shiftKey) { e.preventDefault(); doUndo(); } else if (k === 'z' && !e.shiftKey) { e.preventDefault(); doUndo(); }
@@ -30,6 +29,7 @@ window.addEventListener('keydown', e => {
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;
case 'g': GAME.replace = true; hudDirty = true; break;
case 'h': toggleLens(); 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 '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; case 'l': bot.lamp = !bot.lamp; playSfx('ui'); break;
@@ -43,9 +43,9 @@ window.addEventListener('keydown', e => {
case 'Tab': $('bagpanel').hidden = !$('bagpanel').hidden; break; 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('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.ctrl = false; GAME.selDrag = null; endRmbRecord(); }); window.addEventListener('blur', () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.replace = 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'); } }); 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('mouseleave', () => { GAME.mouse.in = false; });
canvas.addEventListener('contextmenu', e => e.preventDefault()); canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('mousedown', e => { canvas.addEventListener('mousedown', e => {
@@ -57,7 +57,7 @@ canvas.addEventListener('mousedown', e => {
else if (e.button === 1) { e.preventDefault(); pickUnderCursor(); } else if (e.button === 1) { e.preventDefault(); pickUnderCursor(); }
}); });
window.addEventListener('mouseup', e => { 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(); } else if (e.button === 2) { GAME.rmb = false; endRmbRecord(); }
}); });
canvas.addEventListener('wheel', e => { canvas.addEventListener('wheel', e => {
@@ -74,6 +74,6 @@ document.addEventListener('visibilitychange', () => {
if (document.hidden) { if (SET.muteUnfocused) AU.ctx.suspend(); } if (document.hidden) { if (SET.muteUnfocused) AU.ctx.suspend(); }
else AU.ctx.resume(); 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 // while a world is open, ask the browser to confirm leaving
window.addEventListener('beforeunload', e => { if (GAME.running) { e.preventDefault(); e.returnValue = ''; } }); window.addEventListener('beforeunload', e => { if (GAME.running) { e.preventDefault(); e.returnValue = ''; } });
+1 -1
View File
@@ -74,7 +74,7 @@ function frame(now) {
drawRain(); drawRain();
drawBot(); drawBot();
if (!GAME.hidePreviews) drawPreviews(); // tools/shot.py hides them for clean screenshots 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(); renderBuildBar();
if (hudDirty || R.frame % 6 === 0) { renderSelCard(); renderBag(); renderWallet(); hudDirty = false; } if (hudDirty || R.frame % 6 === 0) { renderSelCard(); renderBag(); renderWallet(); hudDirty = false; }
if (R.frame % 4 === 0) renderInfo(); if (R.frame % 4 === 0) renderInfo();
+1 -1
View File
@@ -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'], ['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'], ['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 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'], ['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 = [
+3 -3
View File
@@ -176,7 +176,7 @@ function drawPreviews() {
const afford = canAfford(pieceCostNow(P), cells.length); const afford = canAfford(pieceCostNow(P), cells.length);
for (const [bx, by] of cells) { for (const [bx, by] of cells) {
const err = canPlaceBlock(bx, by), rb = by * BW + bx; 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 ghost = !blockInReach(bx, by) && (!err || /bar/.test(err)); // out of reach: it will be a ghost
const ok = (!err || swap) && !ghost && afford; const ok = (!err || swap) && !ghost && afford;
const [x, y, w, h] = blockRectPx(bx, by, 1, 1); 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 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 { bx, by } = stampOrigin(bw, bh);
const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by); 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 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); 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); } 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.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(); 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); 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) // a small labelled tag drawn on the world (screen pixels)
+1 -1
View File
@@ -40,7 +40,7 @@ function loadCollect() { try { const a = JSON.parse(localStorage.getItem('deepgr
const GAME = { const GAME = {
running: false, paused: false, tool: 0, dropMat: 0, matChosen: false, wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {}, 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 }, 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 }, 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: '', lastAutosave: 0, playTime: 0, lastHud: 0, worldName: '',
}; };
+18 -13
View File
@@ -27,7 +27,7 @@ with sync_playwright() as p:
ev = pg.evaluate ev = pg.evaluate
look = sys.argv[2] if len(sys.argv) > 2 else 'pixel' 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')) 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; })()" 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('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; }})()')) 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('Escape'); settle(100)
pg.keyboard.press('Control+z'); settle(300) 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]')) 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'") ok = select('3', "piece().id === 'fan'")
pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400) pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400)
pg.mouse.down(); pg.mouse.up(); settle(100) 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')) check('a fan on a belt says to hold G', 'Hold G' in ev('GAME.status'), ev('GAME.status'))
pg.keyboard.down('Control'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200) pg.keyboard.down('g'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200)
check('holding Ctrl shows replace mode', 'Replace' in ev("$('buildbar').textContent")) check('holding G shows replace mode', 'Replace' in ev("$('buildbar').textContent"))
pg.screenshot(path=str(out / '07g-replace-fan.png')) pg.screenshot(path=str(out / '07g-replace-fan.png'))
pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('Control'); settle(100) pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('g'); 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')) 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 # pipes are laid by dragging, like belts
ok = select('3', "piece().id === 'pipe'") 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'])) 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) 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')) 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 };') ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };')
ok = select('3', "piece().id === 'filter'") ok = select('3', "piece().id === 'filter'")
check('select filter', ok) check('select filter', ok)
@@ -375,16 +380,16 @@ with sync_playwright() as p:
drag(lambda: blk(bbx - 6, fy), lambda: blk(bbx - 3, fy)) drag(lambda: blk(bbx - 6, fy), lambda: blk(bbx - 3, fy))
nf = ev('blockP.reduce((a, v) => a + (v === BP_FILTER), 0)') 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}))')}") 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);") 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)) 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')) 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('Control'); settle(50) pg.keyboard.down('g'); settle(50)
pg.mouse.move(*blk(bbx - 2, fy)); settle(300) pg.mouse.move(*blk(bbx - 2, fy)); settle(300)
pg.screenshot(path=str(out / '07c-replace-preview.png')) pg.screenshot(path=str(out / '07c-replace-preview.png'))
drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy)) drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy))
pg.keyboard.up('Control'); settle(50) pg.keyboard.up('g'); 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')) 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) 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()) 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);") 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);")