Vacuum menu on F replaces the pick-up modes (t16)
Pick-up takes everything unless something is marked; F with the Vacuum (when nothing in reach can be used) or the card chips open a menu with pick-up and blow toggles per material. The bag rows lose their buttons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+8
-3
@@ -43,7 +43,7 @@ decides something new, add it under "Decisions" with the date.
|
||||
## The core loop
|
||||
|
||||
1. Dig with the resonance drill. Rock shatters into loose grains: rubble, ore, coal, clay, sand.
|
||||
2. Vacuum up what you want (mark materials with ⬇ to take only those), and blow it out where you need it.
|
||||
2. Vacuum up what you want (mark materials in the vacuum menu, F, to take only those), and blow it out where you need it.
|
||||
3. Build a furnace from wall blocks, with a grate floor over a mold. Charge it with ore and coal,
|
||||
light it, and let the molten metal run into the mold.
|
||||
4. Bars drop out of the bottom of the mold; collect them and sell them at the trade dock, or fill buyers' orders, which pay more.
|
||||
@@ -127,8 +127,8 @@ above, including:
|
||||
- **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 marked materials** in Only marked mode, even if the drag starts on
|
||||
something else. The t6 controls below replace the shared ticks.
|
||||
- **The vacuum takes only marked materials** once any are marked, even if the drag starts on
|
||||
something else. The t6 controls below replace the shared ticks; t16 replaces t6's modes.
|
||||
- **The Refiner** grinds stone, granite and basalt rubble, gravel, sand and dirt for trace ore,
|
||||
roughly halving the waste. Ore and anything else it cannot grind drop straight through.
|
||||
- **Pipes, pumps and outlets** move liquids up to 500°C, one liquid per line. A line is stored as
|
||||
@@ -225,6 +225,11 @@ above, including:
|
||||
As built: T or the chip cycles What I click → Only marked → Everything; adding a mark switches
|
||||
to Only marked and removing the last goes back to What I click. A bag name or Q/E blows one
|
||||
material; ⬆ marks build an even mix (bars are never mixed).
|
||||
- **The vacuum menu replaces the modes (t16, 2026-10-24).** Pick-up has no mode: nothing marked
|
||||
takes everything, anything marked takes only those, Shift takes everything for one click. F with
|
||||
the Vacuum (when nothing in reach can be used) or a click on the card opens a menu with ⬇ Pick up
|
||||
and ⬆ Blow toggles per material; the bag rows lose their buttons. T no longer does anything for
|
||||
the vacuum. The owner found t6's version had too many modes.
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
|
||||
+5
-5
@@ -366,7 +366,7 @@ Audio: chiptune effects and generative music that changes with depth. Everything
|
||||
- functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler
|
||||
- variables: AU, SFX, SFX_NEAR, SFX_FAR, ZONES
|
||||
|
||||
### src/ui/state.js (69 lines)
|
||||
### src/ui/state.js (67 lines)
|
||||
Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line.
|
||||
- functions: loadSettings, saveSettings, loadCollect, el, esc, rgbOf, fmtT, toast, setStatus
|
||||
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
|
||||
@@ -404,10 +404,10 @@ The build menu along the bottom: categories, piece tiles and their icons.
|
||||
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
|
||||
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
|
||||
|
||||
### src/ui/selection.js (263 lines)
|
||||
### src/ui/selection.js (270 lines)
|
||||
The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
|
||||
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, setPickMode, cyclePickMode, toggleCollect, toggleBlow, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
||||
- variables: PICK_MODES, PICK_LABELS, lastBagKey
|
||||
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, toggleBlow, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openVacuumMenu, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
||||
- variables: lastBagKey
|
||||
|
||||
### src/ui/info.js (171 lines)
|
||||
Discovery cards, hover info (I) and the cursor label.
|
||||
@@ -418,7 +418,7 @@ Keyboard and mouse listeners.
|
||||
- functions: isTyping
|
||||
- variables: canvas
|
||||
|
||||
### src/ui/actions.js (239 lines)
|
||||
### src/ui/actions.js (233 lines)
|
||||
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
||||
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
||||
- variables: ovShown
|
||||
|
||||
@@ -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-24: t16 Vacuum tool rework, again (Claude Code, Codex)
|
||||
|
||||
- The owner: too many modes; chose an F menu with two pick-up states.
|
||||
- `src/ui/selection.js`: `openVacuumMenu()` (a tile per material with ⬇ Pick up / ⬆ Blow, plus
|
||||
Pick up everything and Blow one at a time); `GAME.pickMode`, `setPickMode`, `cyclePickMode`,
|
||||
`GAME.vacLocked` and the 'first' locking removed. Bag rows lose ⬇/⬆ buttons (a ⬇ tag shows
|
||||
marks). `actions.js`: pick-up is everything unless marked; F with the vacuum opens the menu
|
||||
when nothing in reach can be used. Card chips open the menu (`build-menu.js`). T and gamepad B
|
||||
no longer change vacuum modes. Text in `notebook.js`, `progress.js`, `session.js` (Welcome),
|
||||
`previews.js`; CSS `.vpick`.
|
||||
- `test/play.py`: mode checks replaced (no marks takes everything, marks, Shift, menu toggles,
|
||||
F order, card chip, no bag buttons, menu screenshots at 1280 and 900); `test/t_basics.js` name.
|
||||
- Tests: `run_all.py --browser` all pass (208 `play.py` checks). Codex ran out of credits before
|
||||
its final test run; Claude fixed one check (it mutated a mold's kind) and the suction line.
|
||||
|
||||
## 2026-10-24: t6 Rework the vacuum/blower tool, with t15 (Claude Code, Codex)
|
||||
|
||||
- The owner folded t15 (a material menu for the vacuum) into t6's chosen design.
|
||||
|
||||
+2
-2
@@ -22,12 +22,12 @@ const STARTER_DESIGNS = [
|
||||
|
||||
// ---------- Milestones ----------
|
||||
const MILESTONES = [
|
||||
{ id: 'dig', title: 'Break ground', msg: 'Broken material falls loose. The Vacuum (2) picks up only what you click on.', hint: 'Hold the left mouse button with the Drill (1): it sends pulses at the cursor that shake rock until it shatters. It falls as rubble, ore and coal.' },
|
||||
{ id: 'dig', title: 'Break ground', msg: 'Broken material falls loose. The Vacuum (2) picks up everything, or only what you mark in its menu (F).', hint: 'Hold the left mouse button with the Drill (1): it sends pulses at the cursor that shake rock until it shatters. It falls as rubble, ore and coal.' },
|
||||
{ id: 'fire', title: 'First fire', hint: 'Vacuum up coal or wood chunks, blow a pile out with right click, then hold the Igniter (4) on it. Fuel only burns where it touches open air.', reward: 'Thermal lens unlocked. Press H to see heat.' },
|
||||
{ id: 'brick', title: 'Fired brick', hint: 'Clay turns into brick after a while above about 900°C, hotter than an open wood fire gets. Blow clay lumps into a coal fire, best walled in like a furnace, and vacuum up the brick rubble. Brick rubble builds brick blocks.', reward: 'Brick insulates far better than stone and survives 1750°C.' },
|
||||
{ id: 'charcoal', title: 'Charcoal', hint: 'Wood that gets hot without air turns into charcoal instead of burning. Bury a burning pile of wood chunks under dirt, or heat a sealed box of wood chunks from outside.', reward: 'A fuel you can grow: nearly as hot as coal.' },
|
||||
{ id: 'grow', title: 'Plant a tree', msg: 'Wood is renewable. Plant rows of acorns on dirt to farm it.', hint: 'Cutting down a tree drops acorns. Leave one on dirt or grass under open sky and it sprouts, then grows into a tree in about a minute and a half.' },
|
||||
{ id: 'copper', title: 'Molten copper', msg: 'Molten copper sinks to the bottom of the charge. Get it into a mold before it sets.', hint: 'Copper ore melts at 1085°C. An open fire loses too much heat, so wall coal and ore in with blocks and leave a small vent at the top for smoke. Mix ore and coal about half and half: click coal in your bag, then mark copper ore with ⬆. Right click blows an even mix.' },
|
||||
{ id: 'copper', title: 'Molten copper', msg: 'Molten copper sinks to the bottom of the charge. Get it into a mold before it sets.', hint: 'Copper ore melts at 1085°C. An open fire loses too much heat, so wall coal and ore in with blocks and leave a small vent at the top for smoke. Mix ore and coal about half and half: in the vacuum menu (F), mark coal and copper ore with ⬆. Right click blows an even mix.' },
|
||||
{ id: 'bars', title: 'Copper bars', msg: 'Bars sell for far more than the ore in them, and make metal wall blocks.', hint: 'Build an Ingot mold (Build, 3) right under the furnace with a floor of clay Grates between them: molten copper drips through, while the charge and the thick slag stay up. When the bars set, they drop out of the bottom of the mold onto whatever is under it; a belt carries them off. Leave space below, or vacuum up the bars. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap. Copper that sets in the furnace melts again next firing.' },
|
||||
{ id: 'bit1', title: 'Copper drill bit', hint: 'Cast copper in a Bit mold. Building the furnace right on top works. A Grate over the funnel keeps the charge out. Let it set, then press F.', reward: 'Copper bit. You can now dig iron ore.' },
|
||||
{ id: 'auto', title: 'Moving parts', msg: 'Machines can now haul for you. Feed furnaces automatically, and lift ore out of your mines.', hint: 'Belts carry grains and bars resting on them. Screw tubes push loose material along (never liquids); stacked pointing up they lift it out of a mine. A Filter lets one material fall through and carries the rest along, so you can sort ore from rubble. Move 300 grains with machines.' },
|
||||
|
||||
+15
-10
@@ -177,8 +177,7 @@ table.mt td.dip { color: var(--melt); }
|
||||
#baglist .bagrow.on { border-color: var(--patina-dim); background: rgba(92, 186, 166, 0.08); }
|
||||
#baglist .bagrow .pick { flex: 1; min-width: 0; }
|
||||
#baglist .bagrow .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#baglist .mark { flex: none; width: 20px; height: 20px; padding: 0; justify-content: center; color: var(--ash-dim); }
|
||||
#baglist .mark.on { color: var(--patina); }
|
||||
#baglist .tag { flex: none; padding: 0 3px; color: var(--patina); font-size: 11px; }
|
||||
#selcard .vacrow { flex-wrap: wrap; gap: 4px; }
|
||||
#selcard .vacrow .sw { width: 9px; height: 9px; }
|
||||
#baglist .baghint { color: var(--ash-dim); font-size: 0.82em; padding: 4px 4px 0; }
|
||||
@@ -316,14 +315,14 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
|
||||
.toast { animation: none; }
|
||||
}
|
||||
/* wall material picker */
|
||||
.wpick { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; max-height: min(60vh, 490px); overflow-y: auto; padding: 2px; }
|
||||
.wpick .wp { display: flex; align-items: flex-start; gap: 8px; text-align: left; padding: 8px; border-radius: 6px; border: 1px solid var(--soot-4); background: var(--soot-2); color: var(--bone); font: inherit; font-size: 12px; cursor: pointer; min-width: 0; }
|
||||
.wpick .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
||||
.wpick .wp span span { display: block; color: var(--ash); margin-top: 3px; }
|
||||
.wpick .wp.short { opacity: 0.55; }
|
||||
.wpick .wp:hover { background: #2d2724; }
|
||||
.wpick .wp.on { border-color: #e59a52; background: #33261c; }
|
||||
.wpick .wp:focus-visible, [data-wall-picker]:focus-visible { outline: 2px solid #e59a52; outline-offset: 1px; }
|
||||
:is(.wpick, .vpick) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; max-height: min(60vh, 490px); overflow-y: auto; padding: 2px; }
|
||||
:is(.wpick, .vpick) .wp { display: flex; align-items: flex-start; gap: 8px; text-align: left; padding: 8px; border-radius: 6px; border: 1px solid var(--soot-4); background: var(--soot-2); color: var(--bone); font: inherit; font-size: 12px; cursor: pointer; min-width: 0; }
|
||||
:is(.wpick, .vpick) .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
||||
:is(.wpick, .vpick) .wp span span { display: block; color: var(--ash); margin-top: 3px; }
|
||||
:is(.wpick, .vpick) .wp.short { opacity: 0.55; }
|
||||
:is(.wpick, .vpick) .wp:hover { background: #2d2724; }
|
||||
:is(.wpick, .vpick) .wp.on { border-color: #e59a52; background: #33261c; }
|
||||
:is(.wpick, .vpick) .wp:focus-visible, [data-wall-picker]:focus-visible { outline: 2px solid #e59a52; outline-offset: 1px; }
|
||||
[data-wall-picker] { cursor: pointer; }
|
||||
/* filter picker */
|
||||
.fpick { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; max-height: min(52vh, 420px); overflow-y: auto; padding: 2px; }
|
||||
@@ -362,3 +361,9 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
|
||||
.world-actions input { flex: 1; min-width: 120px; max-width: 100%; }
|
||||
#m-dialog { z-index: 21; }
|
||||
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
|
||||
|
||||
.vpick { max-height: min(48vh, 390px); }
|
||||
.vtoggles { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
||||
.vtoggles button { padding: 4px 7px; font-size: 11px; }
|
||||
.vtoggles button.on { border-color: #e59a52; background: #533820; color: var(--bone); }
|
||||
.vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; }
|
||||
|
||||
+4
-10
@@ -44,6 +44,7 @@ function doInteract() {
|
||||
if (msg) break outer;
|
||||
}
|
||||
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
|
||||
else if (toolId() === 'vacuum') openVacuumMenu();
|
||||
else if ((moldAt(ax, ay) || {}).kind !== 'ingot') setStatus('Nothing to use there.', 'info'); // an ingot mold needs no F: say nothing
|
||||
}
|
||||
// Pick (middle mouse): the piece under the cursor becomes the one you build, with its
|
||||
@@ -118,10 +119,7 @@ function primaryPress(shift) {
|
||||
return;
|
||||
}
|
||||
if (t === 'vacuum') {
|
||||
const g = GAME.pickMode === 'first' ? grainAt(ax, ay) : 0;
|
||||
GAME.vacFilter = shift || GAME.pickMode === 'all' ? -1 : g;
|
||||
GAME.vacLocked = shift || GAME.pickMode !== 'first' || !!g;
|
||||
if (!shift && GAME.pickMode === 'marked' && !GAME.collect.size) setStatus('Nothing marked to pick up. R over something, or ⬇ in the bag.', 'info');
|
||||
GAME.vacFilter = shift || !GAME.collect.size ? -1 : 0;
|
||||
hudDirty = true;
|
||||
} else if (t === 'build') {
|
||||
const P = piece();
|
||||
@@ -188,7 +186,7 @@ function primaryRelease(shift) {
|
||||
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;
|
||||
}
|
||||
if (t === 'vacuum') { GAME.vacFilter = 0; GAME.vacLocked = false; hudDirty = true; }
|
||||
if (t === 'vacuum') { GAME.vacFilter = 0; hudDirty = true; }
|
||||
}
|
||||
function secondaryPress() {
|
||||
const t = toolId();
|
||||
@@ -207,13 +205,9 @@ function toolTick() {
|
||||
const r = toolPulse(ax, ay, GAME.drillW, GAME.drillTune);
|
||||
if (r.fired) { playSfx('pulse', GAME.drillW); if (r.status) setStatus(r.status); }
|
||||
} else if (t === 'vacuum') {
|
||||
if (!GAME.vacLocked && GAME.pickMode === 'first') { const g = grainAt(ax, ay); if (g) { GAME.vacFilter = g; GAME.vacLocked = true; hudDirty = true; } }
|
||||
const f = GAME.vacFilter, marked = GAME.pickMode === 'marked', list = GAME.collect;
|
||||
if (f || marked && list.size) {
|
||||
const r = toolVacuum(ax, ay, GAME.brush[1], f < 0 ? () => true : marked ? m => list.has(m) : m => m === f);
|
||||
const r = toolVacuum(ax, ay, GAME.brush[1], GAME.vacFilter < 0 || !GAME.collect.size ? () => true : m => GAME.collect.has(m));
|
||||
if (r.got) { playSfx('suck'); hudDirty = true; }
|
||||
if (r.status) setStatus(r.status);
|
||||
}
|
||||
} else if (t === 'ignite') {
|
||||
if (inReach(ax, ay)) { const r = toolIgnite(ax, ay); playSfx('spark'); if (Math.random() < 0.6) spawnSpark(ax + 0.5, ay + 0.5); if (!r.fuel) setStatus('Aim at coal or wood touching air to light it.'); }
|
||||
else setStatus('Out of reach.');
|
||||
|
||||
@@ -86,7 +86,7 @@ function renderBuildBar() {
|
||||
pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`;
|
||||
}
|
||||
function setupBuildBar() {
|
||||
$('selcard').addEventListener('click', e => { if (e.target.closest('[data-wall-picker]')) openWallPicker(); else if (e.target.closest('[data-pick-mode]')) cyclePickMode(); });
|
||||
$('selcard').addEventListener('click', e => { if (e.target.closest('[data-wall-picker]')) openWallPicker(); else if (e.target.closest('[data-vac-menu]')) openVacuumMenu(); });
|
||||
const bar = $('buildbar'); if (!bar) return;
|
||||
bar.addEventListener('click', e => {
|
||||
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ function pollPad() {
|
||||
if (padPressed(gp, 0)) doInteract();
|
||||
if (padPressed(gp, 2)) rotate();
|
||||
if (padPressed(gp, 3)) toggleLens();
|
||||
if (padPressed(gp, 1)) { if (GAME.drag) GAME.drag = null; else if (GAME.paste) { GAME.paste = false; hudDirty = true; } else if (toolId() === 'vacuum') cyclePickMode(); }
|
||||
if (padPressed(gp, 1)) { if (GAME.drag) GAME.drag = null; else if (GAME.paste) { GAME.paste = false; hudDirty = true; } }
|
||||
if (padPressed(gp, 10)) bot.lamp = !bot.lamp;
|
||||
if (padPressed(gp, 11)) { SET.zoom = SET.zoom >= 10 ? 2 : SET.zoom + 2; saveSettings(); resizeRender(); R.snapNext = true; }
|
||||
if (padPressed(gp, 8)) openNotebook();
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ window.addEventListener('keydown', e => {
|
||||
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') cyclePickMode(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
||||
case 't': if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
||||
case 'f': doInteract(); break;
|
||||
case 'g': GAME.replace = true; hudDirty = true; break;
|
||||
case 'h': toggleLens(); break;
|
||||
|
||||
+5
-5
@@ -170,18 +170,18 @@ 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'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'],
|
||||
['Mark/unmark what is under the cursor (Vacuum)', 'R'], ['Pick-up mode (Vacuum)', 'T'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
||||
['Mark/unmark what is under the cursor (Vacuum)', 'R'], ['Vacuum menu: what to pick up and blow', 'F (with the Vacuum, away from anything to use)'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
||||
['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 = [
|
||||
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
|
||||
['Cycle what you blow · piece', 'D-pad ← →'], ['Brush size', 'D-pad ↑ ↓'], ['Interact', 'A'], ['Variant · mark material', 'X'], ['Pick-up mode (Vacuum)', 'B'], ['Thermal lens', 'Y'],
|
||||
['Cycle what you blow · piece', 'D-pad ← →'], ['Brush size', 'D-pad ↑ ↓'], ['Interact', 'A'], ['Variant · mark material', 'X'], ['Thermal lens', 'Y'],
|
||||
['Cancel', 'B'], ['Zoom', 'R3'], ['Lamp', 'L3'], ['Notebook', 'View / Back'], ['Menu', 'Menu / Start'],
|
||||
];
|
||||
const tools = [
|
||||
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
|
||||
['2 Vacuum', 'Left click picks up what you click, only marked materials, or everything: T or the card chip changes the mode. R over a grain or ⬇ in the bag marks it for pick-up. Adding a mark switches to Only marked; removing the last returns to What I click. Hold Shift while not flying to take everything. Right click blows the materials marked ⬆ in the bag as an even mix. Bars blow one at a time. Click a name or use Q/E to blow only one material; it stays chosen when empty. Pick-up and blow marks are separate.'],
|
||||
['2 Vacuum', 'Left click picks up everything, or only marked materials once you mark some. F opens the vacuum menu to mark what to pick up (⬇) and blow (⬆). R over a grain marks it. Hold Shift while not flying to take everything. Right click blows the ⬆ materials as an even mix, bars one at a time. Click a bag name or use Q/E to blow only one; it stays chosen when empty.'],
|
||||
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps and outlets, machines (fans, the Refiner) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'],
|
||||
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
|
||||
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
|
||||
@@ -191,9 +191,9 @@ function controlsHtml() {
|
||||
<p class="label grouphead">Keyboard and mouse</p>${tbl(kb)}<p class="label grouphead">Gamepad</p>${tbl(pad)}
|
||||
<p class="label grouphead">First steps</p>
|
||||
<p class="note">1. Drill down a little. Stone falls as rubble; coal (dark seams), copper ore (green speckles) and clay (reddish) come loose too.<br>
|
||||
2. With the Vacuum, click on coal to suck up only coal, then the copper ore, rubble and clay you want.<br>
|
||||
2. With the Vacuum, left click to pick up loose materials. To take only coal, copper ore, rubble or clay, mark what you want in the vacuum menu (F).<br>
|
||||
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.<br>
|
||||
4. Click coal in your bag, then mark copper ore with ⬆ to blow both. Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
||||
4. In the vacuum menu (F), mark coal and copper ore with ⬆ to blow both. Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
||||
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or vacuum them up. Leftover metal stays for the next firing. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.<br>
|
||||
Screw tubes carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth.<br>
|
||||
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.<br>
|
||||
|
||||
+2
-2
@@ -153,9 +153,9 @@ function drawPreviews() {
|
||||
if (t === 'vacuum') {
|
||||
// what the right button would blow, next to the cursor; what the left is sucking, while it sucks
|
||||
const sz = Math.max(6, zp * 1.6), qx = p.x + (r + 1.2) * zp, qy = p.y - (r + 1.2) * zp;
|
||||
const swatches = GAME.lmb && GAME.pickMode === 'first' && GAME.vacFilter > 0 ? [GAME.vacFilter] : GAME.lmb && GAME.pickMode === 'marked' && GAME.vacFilter >= 0 ? [...GAME.collect] : blowList();
|
||||
const swatches = GAME.lmb ? GAME.vacFilter >= 0 && GAME.collect.size ? [...GAME.collect] : [] : blowList();
|
||||
swatches.slice(0, 4).forEach((m, k) => { ctx.fillStyle = rgbOf(m); ctx.fillRect(qx + k * (sz + 2), qy, sz, sz); });
|
||||
if (blowing || (GAME.lmb && GAME.vacFilter)) {
|
||||
if (blowing || GAME.lmb) {
|
||||
// faint line from the nozzle: the path the grains take
|
||||
const c = worldToScreenPx(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||
ctx.strokeStyle = blowing ? 'rgba(255,210,122,0.22)' : 'rgba(92,186,166,0.22)'; ctx.lineWidth = Math.max(1, zp * (r + 0.5) * 0.6);
|
||||
|
||||
+34
-27
@@ -53,23 +53,10 @@ function rotate() {
|
||||
else return;
|
||||
playSfx('ui'); hudDirty = true;
|
||||
}
|
||||
const PICK_MODES = ['first', 'marked', 'all'];
|
||||
const PICK_LABELS = { first: 'What I click', marked: 'Only marked', all: 'Everything' };
|
||||
function setPickMode(mode) {
|
||||
GAME.pickMode = mode;
|
||||
try { localStorage.setItem('deepgrain.pickMode', mode); } catch (e) { /* storage unavailable */ }
|
||||
hudDirty = true;
|
||||
}
|
||||
function cyclePickMode() {
|
||||
setPickMode(PICK_MODES[(PICK_MODES.indexOf(GAME.pickMode) + 1) % PICK_MODES.length]);
|
||||
playSfx('ui');
|
||||
}
|
||||
function toggleCollect(m) {
|
||||
if (!m) return;
|
||||
const add = !GAME.collect.has(m);
|
||||
if (add) GAME.collect.add(m); else GAME.collect.delete(m);
|
||||
if (add && GAME.pickMode !== 'marked') { setPickMode('marked'); setStatus('Picking up only marked materials.', 'info'); }
|
||||
else if (!GAME.collect.size && GAME.pickMode === 'marked') setPickMode('first');
|
||||
try { localStorage.setItem('deepgrain.collect', JSON.stringify([...GAME.collect])); } catch (e) { /* storage unavailable */ }
|
||||
lastBagKey = ''; hudDirty = true; playSfx('ui');
|
||||
}
|
||||
@@ -154,6 +141,34 @@ function openFilterPicker(cur, onDone, title) {
|
||||
box.querySelectorAll('[data-m]').forEach(b => b.onclick = () => { const m = +b.dataset.m; if (sel.has(m)) sel.delete(m); else sel.add(m); playSfx('ui'); sync(); });
|
||||
sync();
|
||||
}
|
||||
function openVacuumMenu() {
|
||||
ensureBlow();
|
||||
dialog('Vacuum', '', [
|
||||
{ label: 'Pick up everything', cls: 'ghost', fn: () => {
|
||||
GAME.collect.clear();
|
||||
try { localStorage.setItem('deepgrain.collect', '[]'); } catch (e) { /* storage unavailable */ }
|
||||
lastBagKey = ''; hudDirty = true; playSfx('ui'); sync(); return true;
|
||||
} },
|
||||
{ label: 'Blow one at a time', cls: 'ghost', fn: () => {
|
||||
GAME.blow = new Set(blowList().slice(0, 1));
|
||||
lastBagKey = ''; hudDirty = true; playSfx('ui'); sync(); return true;
|
||||
} },
|
||||
{ label: 'Done', cls: 'primary' },
|
||||
]);
|
||||
const box = $('dlg-body');
|
||||
function sync() {
|
||||
const list = BAG_ORDER.filter(m => bag[m] > 0)
|
||||
.concat(BAG_ORDER.filter(m => !bag[m] && (GAME.collect.has(m) || GAME.blow.has(m))))
|
||||
.concat(BAG_ORDER.filter(m => !bag[m] && !GAME.collect.has(m) && !GAME.blow.has(m) && seen[m] && filterable(m)));
|
||||
const blow = blowList();
|
||||
box.innerHTML = `<p class="note" id="vm-sum">${esc(GAME.collect.size ? 'Picking up only ' + matList([...GAME.collect]) + '.' : 'Picking up everything.')} Blowing out ${blow.length > 1 ? `an even mix of ${blow.length}` : blow.length ? esc(MD[blow[0]].name.toLowerCase()) : 'nothing'}.</p>`
|
||||
+ (!GAME.collect.size ? '<p class="note">Mark materials to pick up only those.</p>' : '')
|
||||
+ (list.length ? `<div class="vpick">${list.map(m => `<div class="wp"><span class="sw" style="background:${rgbOf(m)}"></span><span><b>${esc(MD[m].name)}</b><span>${bag[m] ? `you have ${bag[m].toLocaleString('en-US')}` : 'none in bag'}</span><div class="vtoggles"><button data-vm-pick="${m}" aria-pressed="${GAME.collect.has(m)}" class="btn ${GAME.collect.has(m) ? 'on' : ''}">⬇ Pick up</button><button data-vm-blow="${m}" aria-pressed="${GAME.blow.has(m)}" class="btn ${GAME.blow.has(m) ? 'on' : ''}"${!bag[m] && !GAME.blow.has(m) ? ' disabled' : ''}>⬆ Blow</button></div></span></div>`).join('')}</div>` : '<p class="note">Find something loose first.</p>');
|
||||
box.querySelectorAll('[data-vm-pick]').forEach(b => b.onclick = () => { toggleCollect(+b.dataset.vmPick); sync(); });
|
||||
box.querySelectorAll('[data-vm-blow]').forEach(b => b.onclick = () => { toggleBlow(+b.dataset.vmBlow); sync(); });
|
||||
}
|
||||
sync();
|
||||
}
|
||||
function openWallPicker() {
|
||||
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
|
||||
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
|
||||
@@ -211,13 +226,12 @@ function renderSelCard() {
|
||||
+ '<div class="sub"><kbd>Q</kbd>/<kbd>E</kbd> piece · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab · <kbd>R</kbd> turn or material · <kbd>F</kbd> material menu · hold <kbd>G</kbd> replace · right click take back · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo</div>';
|
||||
} else if (t === 'vacuum') {
|
||||
ensureBlow();
|
||||
const list = blowList(), f = GAME.lmb && GAME.pickMode === 'first' ? GAME.vacFilter : 0;
|
||||
const marks = GAME.pickMode === 'marked' ? [...GAME.collect] : f > 0 ? [f] : [];
|
||||
const list = blowList(), marks = [...GAME.collect];
|
||||
const sws = ms => ms.slice(0, 4).map(m => `<span class="sw" title="${esc(MD[m].name)}" style="background:${rgbOf(m)}"></span>`).join('') + (ms.length > 4 ? `+${ms.length - 4}` : '');
|
||||
const count = list.reduce((n, m) => n + bag[m], 0);
|
||||
h = `<div class="row vacrow"><b>Pick up</b><button class="chip hot" data-pick-mode title="Cycle pick-up mode">${PICK_LABELS[GAME.pickMode]} · T</button>${sws(marks)}</div>
|
||||
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd>${sws(list)}<span class="name grow">${list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : 'bag empty'}</span><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none left'}</span><kbd>E</kbd></div>
|
||||
<div class="sub">Left picks up, right blows out. R marks what is under the cursor. Shift takes everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
|
||||
h = `<div class="row vacrow"><b>Pick up</b><button class="chip hot" data-vac-menu>${marks.length ? 'Only marked' : 'Everything'} · F</button>${sws(marks)}</div>
|
||||
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><button class="name grow vacchoice" data-vac-menu>${sws(list)}${list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : 'bag empty'}</button><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none left'}</span><kbd>E</kbd></div>
|
||||
<div class="sub">Left picks up, right blows out. F chooses what. R marks what is under the cursor. Shift takes everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
|
||||
} 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>`;
|
||||
}
|
||||
@@ -240,10 +254,7 @@ function renderBag() {
|
||||
for (const m of l) {
|
||||
const sel = (t === 'vacuum' && GAME.blow.has(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
|
||||
const row = el('div', { class: 'bagrow' + (sel ? ' on' : '') });
|
||||
if (t === 'vacuum') {
|
||||
const down = el('button', { class: 'mark' + (GAME.collect.has(m) ? ' on' : ''), title: `Pick up ${MD[m].name}`, 'aria-label': `Pick up ${MD[m].name}`, 'aria-pressed': GAME.collect.has(m), 'data-pick': m }, '⬇');
|
||||
down.onclick = () => toggleCollect(m); row.appendChild(down);
|
||||
}
|
||||
if (t === 'vacuum' && GAME.collect.has(m)) row.appendChild(el('span', { class: 'tag', title: 'Marked to pick up', 'aria-label': 'Marked to pick up' }, '⬇'));
|
||||
const b = el('button', { class: 'pick', title: WALL_OF[m] && t === 'build' ? `Build with ${MD[m].name}` : `Blow out ${MD[m].name}` });
|
||||
b.innerHTML = `<span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${esc(MD[m].name)}</span><span class="n num">${bag[m].toLocaleString('en-US')}</span>`;
|
||||
b.onclick = () => {
|
||||
@@ -253,11 +264,7 @@ function renderBag() {
|
||||
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
||||
};
|
||||
row.appendChild(b);
|
||||
if (t === 'vacuum') {
|
||||
const up = el('button', { class: 'mark' + (GAME.blow.has(m) ? ' on' : ''), title: `Blow out ${MD[m].name}`, 'aria-label': `Blow out ${MD[m].name}`, 'aria-pressed': GAME.blow.has(m), 'data-blow': m }, '⬆');
|
||||
up.onclick = () => toggleBlow(m); row.appendChild(up);
|
||||
}
|
||||
box.appendChild(row);
|
||||
}
|
||||
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, '⬇ what you pick up · ⬆ what you blow out · click a name to blow only that'));
|
||||
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'F with the Vacuum: choose what to pick up and blow'));
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ function startNewWorld(seed, w, h, name = '') {
|
||||
GAME.playTime = 0; GAME.clip = null; GAME.paste = false; GAME.selDrag = null; GAME.tool = 0; GAME.blow.clear(); GAME.wallMat = RSTONE; GAME.filterCfg = { m: [], inv: 0 }; GAME.pieceIdx = 0;
|
||||
showLoading(false);
|
||||
enterGame();
|
||||
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Vacuum (2) sucks up what you click on and blows it back out with right click.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
|
||||
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Vacuum (2) picks up everything, or only what you mark in its menu (F). Right click blows materials back out.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
|
||||
await saveWorld('auto');
|
||||
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
||||
}, 30);
|
||||
|
||||
+1
-3
@@ -39,13 +39,11 @@ const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN
|
||||
function loadCollect() { try { const a = JSON.parse(localStorage.getItem('deepgrain.collect') || '[]'); return new Set(a.filter(m => m > 0 && m < NMAT && BAGOK[m])); } catch (e) { return new Set(); } }
|
||||
const GAME = {
|
||||
running: false, paused: false, tool: 0, blow: new Set(), 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, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
|
||||
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, 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, worldId: '', worldName: '',
|
||||
};
|
||||
try { GAME.pickMode = localStorage.getItem('deepgrain.pickMode'); } catch (e) { /* storage unavailable */ }
|
||||
if (!['first', 'marked', 'all'].includes(GAME.pickMode)) GAME.pickMode = GAME.collect.size ? 'marked' : 'first';
|
||||
const keys = new Set();
|
||||
let hudDirty = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
id: t16
|
||||
status: Review
|
||||
order: 2
|
||||
priority: P3
|
||||
created: 2026-10-24
|
||||
updated: 2026-10-24T06:28:51Z
|
||||
title: vacuum tool rework, again
|
||||
report: |
|
||||
(agent) Fewer modes. Left click picks up everything, or only what you've marked once you mark
|
||||
something; Shift takes everything for one click. F with the Vacuum (when nothing in reach can be
|
||||
used, like a mold or gate) opens the Vacuum menu: a tile per material with ⬇ Pick up and ⬆ Blow,
|
||||
plus Pick up everything and Blow one at a time. Clicking the Pick up / Blow out chips on the card
|
||||
opens it too. The bag rows no longer have buttons (a small ⬇ shows what's marked); clicking a
|
||||
name still blows only that, Q/E still cycles, R still marks under the cursor. T does nothing
|
||||
for the vacuum now.
|
||||
To check: press F with the Vacuum out in the open, mark coal and copper ore to blow, blow a mix;
|
||||
mark copper ore to pick up and vacuum a mixed pile; Pick up everything clears the marks.
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
- (owner's answers, 2026-10-24) What bothers you about the vacuum now? "Too many modes". Direction: "F opens a vacuum menu (Recommended)": F with the vacuum opens a dialog like the wall menu: one tile per material with Pick up and Blow toggles. The ⬇/⬆ buttons leave the bag rows. Pick-up modes shrink to two: Everything, or Only marked.
|
||||
|
||||
## Subtasks
|
||||
|
||||
- [x] Pick-up: everything unless something is marked (no mode variable)
|
||||
- [x] Vacuum menu on F and on the card chips; bag row buttons removed
|
||||
- [x] Text, tests and screenshots updated
|
||||
+50
-51
@@ -21,14 +21,10 @@ with sync_playwright() as p:
|
||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
|
||||
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
|
||||
check('empty marks default to first mode', pg.evaluate("GAME.pickMode === 'first'"))
|
||||
pg.evaluate("toggleCollect(CUORE); setPickMode('all')")
|
||||
pg.evaluate("toggleCollect(CUORE)")
|
||||
pg.reload(); pg.wait_for_timeout(300)
|
||||
check('pick-up marks and a manual mode persist across reload', pg.evaluate("GAME.collect.has(CUORE) && GAME.pickMode === 'all'"))
|
||||
pg.evaluate("localStorage.removeItem('deepgrain.pickMode')")
|
||||
pg.reload(); pg.wait_for_timeout(300)
|
||||
check('old pick-up marks default to marked mode', pg.evaluate("GAME.collect.has(CUORE) && GAME.pickMode === 'marked'"))
|
||||
pg.evaluate("localStorage.removeItem('deepgrain.collect'); localStorage.removeItem('deepgrain.pickMode')")
|
||||
check('pick-up marks persist across reload', pg.evaluate("GAME.collect.has(CUORE)"))
|
||||
pg.evaluate("localStorage.removeItem('deepgrain.collect')")
|
||||
pg.reload(); pg.wait_for_timeout(300)
|
||||
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
|
||||
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
|
||||
@@ -224,7 +220,7 @@ with sync_playwright() as p:
|
||||
pg.screenshot(path=str(out / '01-dug.png'))
|
||||
check('trees live in the background layer', ev('bgm.some(v => v === WOOD)') and ev('(() => { for (let i = 0; i < N; i++) if (mat[i] === LEAVES) return false; return true; })()'))
|
||||
|
||||
# 2. vacuum, left button: only the material you start on
|
||||
# 2. vacuum, left button: everything unless marked
|
||||
pg.keyboard.press('2'); settle(100)
|
||||
tgt = ev(f'''(() => {{ for (let r = 0; r < 30; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
|
||||
const x = {bx + 6} + dx, y = {gy + 3} + dy, m = mat[y*W+x]; if (PH[m] === PH_POWDER && inReach(x, y)) return [x, y, m]; }} return null; }})()''')
|
||||
@@ -236,16 +232,14 @@ with sync_playwright() as p:
|
||||
got = ev(f'bag[{tgt[2]}]')
|
||||
others = ev(f'bagTotal - bag[{tgt[2]}]')
|
||||
check('vacuum (left) picks the clicked material', got > 0, f"{ev(f'MD[{tgt[2]}].name')} x{got}")
|
||||
check('vacuum (left) leaves other materials', others == 0, others)
|
||||
check('vacuum defaults to What I click', ev("GAME.pickMode === 'first'"))
|
||||
check('first mode locks onto dirt and leaves other grains', ev("""(() => {
|
||||
check('unmarked vacuum takes both materials in a mixed pile', ev("""(() => {
|
||||
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, cells = [], dirt = bag[LDIRT], stone = bag[RSTONE];
|
||||
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { const i = (y+dy)*W+x+dx; cells.push([i, mat[i], temp[i], flags[i]]); setCell(i, dx < 0 ? RSTONE : LDIRT, 20, 0); }
|
||||
const aim = GAME.aim; GAME.aim = { x, y }; GAME.lmb = true; primaryPress(false); toolTick();
|
||||
const ok = GAME.vacFilter === LDIRT && bag[LDIRT] > dirt && bag[RSTONE] === stone;
|
||||
const ok = GAME.vacFilter === -1 && bag[LDIRT] > dirt && bag[RSTONE] > stone;
|
||||
primaryRelease(false); GAME.lmb = false; GAME.aim = aim;
|
||||
for (const [i, m, t, f] of cells) setCell(i, m, t, f);
|
||||
bagTake(LDIRT, bag[LDIRT] - dirt); return ok;
|
||||
bagTake(LDIRT, bag[LDIRT] - dirt); bagTake(RSTONE, bag[RSTONE] - stone); return ok;
|
||||
})()"""))
|
||||
check('middle click chooses a loose material even with none in the bag', ev("""(() => {
|
||||
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, i = y*W+x;
|
||||
@@ -262,11 +256,10 @@ with sync_playwright() as p:
|
||||
if tgt2:
|
||||
cx, cy = cell_to_screen(tgt2[0], tgt2[1])
|
||||
pg.mouse.move(cx, cy); settle(80); pg.keyboard.press('r'); settle(80)
|
||||
check('R marks pick-up and switches mode without changing blow', ev(f"GAME.collect.has({tgt2[2]}) && GAME.pickMode === 'marked'") and ev('[...GAME.blow]') == blow_before)
|
||||
check('R marks pick-up without changing blow', ev(f"GAME.collect.has({tgt2[2]})") and ev('[...GAME.blow]') == blow_before)
|
||||
ev('renderBag()'); settle(50)
|
||||
marks = pg.locator('#baglist [data-pick][aria-pressed=true]')
|
||||
if marks.count(): marks.first.click(); settle(80)
|
||||
check('removing the last pick-up mark returns to first', ev(f"!GAME.collect.has({tgt2[2]}) && GAME.pickMode === 'first'"))
|
||||
pg.keyboard.press('r'); settle(80)
|
||||
check('R again unmarks', ev(f"!GAME.collect.has({tgt2[2]})"))
|
||||
pg.screenshot(path=str(out / '02-vacuum.png'))
|
||||
|
||||
# 2b. the vacuum filter is fixed when you click: ore with stone around it, ore marked
|
||||
@@ -290,43 +283,28 @@ with sync_playwright() as p:
|
||||
mixed_pile(px0, py0); settle(400)
|
||||
ore = find_ore()
|
||||
refill('bag.fill(0)')
|
||||
ev('GAME.collect = new Set([CUORE]); setPickMode("marked"); lastBagKey = "";')
|
||||
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
|
||||
suck_at(*ore)
|
||||
check('with ore marked, clicking ore never pulls in stone', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||||
st = ev(f'(() => {{ for (let y = {py0} - 8; y < {gy} + 4; y++) for (let x = {px0} - 6; x < {px0} + 20; x++) if (mat[y*W+x] === RSTONE && mat[(y-1)*W+x] === EMPTY && inReach(x, y)) return [x, y]; return null; }})()')
|
||||
suck_at(*st)
|
||||
check('with ore marked, starting on stone rubble still takes only the ore', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||
ev('GAME.collect = new Set(); setPickMode("first"); lastBagKey = "";')
|
||||
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
|
||||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||||
ore = find_ore()
|
||||
suck_at(*ore, ms=500, shift=True, fly='d')
|
||||
check('Shift held while flying boosts instead of taking everything', ev('bag[RSTONE]') == 0 or ev('bag[CUORE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||
check('Shift held while flying boosts instead of taking everything', ev('bag[RSTONE]') == 0 and ev('bag[CUORE]') > 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||
ev(f'bot.x = {bx} - 2; bot.y = {by} - 2; bot.vx = bot.vy = 0;'); settle(300)
|
||||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||||
ore = find_ore()
|
||||
suck_at(*ore, ms=500, shift=True)
|
||||
check('Shift held standing still takes everything', ev('bag[RSTONE]') > 0 and ev('bag[CUORE]') > 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||
pg.keyboard.press('t'); settle(80)
|
||||
check('T cycles first to marked', ev("GAME.pickMode === 'marked'"))
|
||||
before = ev('bagTotal')
|
||||
suck_at(*ore, ms=100)
|
||||
check('empty marked mode picks up nothing and explains why', ev('bagTotal') == before and 'Nothing marked to pick up. R over something, or ⬇ in the bag.' in ev('GAME.status'))
|
||||
pg.keyboard.press('t'); settle(80)
|
||||
check('T cycles marked to all', ev("GAME.pickMode === 'all'"))
|
||||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||||
ore = find_ore(); suck_at(*ore, ms=500)
|
||||
check('Everything mode picks up both materials', ev('bag[RSTONE] > 0 && bag[CUORE] > 0'))
|
||||
pg.keyboard.press('t'); settle(80)
|
||||
check('T cycles all to first', ev("GAME.pickMode === 'first'"))
|
||||
for mode in ['marked', 'all', 'first']:
|
||||
pg.click('#selcard [data-pick-mode]'); settle(80)
|
||||
check(f'card chip cycles to {mode}', ev(f'GAME.pickMode === "{mode}"'))
|
||||
ev("toggleCollect(CUORE); setPickMode('all'); toggleCollect(RSTONE)")
|
||||
check('adding another mark switches a manual mode back to marked', ev("GAME.pickMode === 'marked'"))
|
||||
ev("setPickMode('all'); toggleCollect(RSTONE)")
|
||||
check('removing a mark keeps a manual mode', ev("GAME.pickMode === 'all'"))
|
||||
ev("GAME.collect.clear(); setPickMode('first'); lastBagKey = ''; hudDirty = true")
|
||||
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
|
||||
ev("GAME.aim = { x: Math.floor(bot.x), y: 1 }")
|
||||
pg.keyboard.press('f'); settle(100)
|
||||
check('F away from usable things opens Vacuum', ev("!$('m-dialog').hidden && $('dlg-title').textContent === 'Vacuum'"))
|
||||
ev("closeModal('m-dialog')")
|
||||
# zoom jumps straight to the bot
|
||||
pg.mouse.move(640, 400); settle(600)
|
||||
pg.mouse.wheel(0, -100); pg.wait_for_timeout(60)
|
||||
@@ -359,32 +337,49 @@ with sync_playwright() as p:
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
check('clicking a bag name chooses one blow material', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
pick_before = ev('[...GAME.collect]')
|
||||
pg.click(f'#baglist [data-blow="{ev("CUORE")}"]'); settle(100)
|
||||
ev('GAME.blow = new Set([RSTONE]); hudDirty = true')
|
||||
pg.click('#selcard [data-vac-menu]'); settle(80)
|
||||
pg.click(f'#dlg-body [data-vm-blow="{ev("COAL")}"]'); settle(80)
|
||||
pg.click(f'#dlg-body [data-vm-blow="{ev("RSTONE")}"]'); settle(80)
|
||||
pg.click(f'#dlg-body [data-vm-blow="{ev("CUORE")}"]'); settle(80)
|
||||
pg.get_by_role('button', name='Done', exact=True).click(); settle(100)
|
||||
check('blow marks make a mix without changing pick-up marks', ev('GAME.blow.has(COAL) && GAME.blow.has(CUORE) && GAME.blow.size === 2') and ev('[...GAME.collect]') == pick_before and 'Mix of 2' in pg.inner_text('#selcard'))
|
||||
blow_before = ev('[...GAME.blow]')
|
||||
pg.click(f'#baglist [data-pick="{ev("RSTONE")}"]'); settle(80)
|
||||
pg.click('#selcard [data-vac-menu]'); settle(80)
|
||||
check('card opens vacuum menu', ev("!$('m-dialog').hidden"))
|
||||
pg.click(f'#dlg-body [data-vm-pick="{ev("RSTONE")}"]'); settle(80)
|
||||
check('menu pick button marks material', ev('GAME.collect.has(RSTONE)'))
|
||||
pg.get_by_role('button', name='Pick up everything', exact=True).click(); settle(80)
|
||||
check('Pick up everything clears marks and keeps menu open', ev("!GAME.collect.size && !$('m-dialog').hidden"))
|
||||
pg.get_by_role('button', name='Done', exact=True).click(); settle(80)
|
||||
check('pick-up marks do not change blow marks', ev('[...GAME.blow]') == blow_before)
|
||||
check('vacuum bag rows have both toggles', pg.locator('#baglist [data-pick]').count() == pg.locator('#baglist .bagrow').count() == pg.locator('#baglist [data-blow]').count())
|
||||
check('vacuum bag has no pick or blow buttons', pg.locator('#baglist [data-pick], #baglist [data-blow]').count() == 0)
|
||||
pg.mouse.move(mx_, my_); pg.mouse.down(button='right'); settle(500); pg.mouse.up(button='right'); settle(600)
|
||||
dc, do_ = 60 - ev('bag[COAL]'), 60 - ev('bag[CUORE]')
|
||||
check('right click blows an even marked ore/coal mix', dc > 5 and abs(dc - do_) <= 2, f'coal {dc}, ore {do_}')
|
||||
pg.keyboard.press('e'); settle(80)
|
||||
check('Q/E replaces the mix with one material', ev('GAME.blow.size === 1'))
|
||||
ev("GAME.collect = new Set([COAL, CUORE]); setPickMode('marked'); GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
|
||||
ev("GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
|
||||
refill('bag[COAL] = 120; bag[CUORE] = 100; bag[RSTONE] = 600')
|
||||
for width, height, suffix in [(1280, 800, ''), (900, 700, '-900')]:
|
||||
pg.set_viewport_size({'width': width, 'height': height}); settle(200)
|
||||
pg.screenshot(path=str(out / f'vacuum-card{suffix}.png'))
|
||||
check(f'vacuum panel fits at {width}', ev("(() => { const c = $('selcard'), b = $('bagpanel'); return [c, b].every(e => { const r = e.getBoundingClientRect(); return r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight && e.scrollWidth <= e.clientWidth; }) && [...$('baglist').querySelectorAll('.bagrow')].every(e => e.scrollWidth <= e.clientWidth); })()"))
|
||||
pg.click('#selcard [data-vac-menu]'); settle(80)
|
||||
pg.screenshot(path=str(out / f'vacuum-menu{suffix}.png'))
|
||||
check(f'vacuum menu fits at {width}', ev("(() => { const e = $('dlg-body'), r = e.closest('.plate').getBoundingClientRect(); return e.scrollWidth <= e.clientWidth && document.querySelector('.vpick').scrollWidth <= document.querySelector('.vpick').clientWidth && r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight; })()"))
|
||||
pg.get_by_role('button', name='Blow one at a time', exact=True).click(); settle(80)
|
||||
check(f'Blow one at a time keeps menu open at {width}', ev("GAME.blow.size === 1 && !$('m-dialog').hidden"))
|
||||
pg.get_by_role('button', name='Done', exact=True).click(); settle(80)
|
||||
pg.set_viewport_size({'width': 1280, 'height': 800})
|
||||
pg.keyboard.press('3'); settle(80)
|
||||
check('build bag has no vacuum toggles', pg.locator('#baglist .mark').count() == 0)
|
||||
pg.keyboard.press('2'); settle(80)
|
||||
ev("toggleBlow(BARCU)")
|
||||
ev("GAME.blow = new Set([COAL, CUORE]); toggleBlow(BARCU)")
|
||||
check('a bar blow mark replaces the mix', ev('GAME.blow.size === 1 && GAME.blow.has(BARCU)'))
|
||||
ev("toggleBlow(COAL)")
|
||||
check('a grain blow mark replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
ev("GAME.collect.clear(); setPickMode('first'); lastBagKey = ''; hudDirty = true")
|
||||
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
|
||||
refill('bag[COAL] = 120')
|
||||
|
||||
# 4. igniter lights it
|
||||
@@ -510,7 +505,13 @@ with sync_playwright() as p:
|
||||
pg.keyboard.press('2'); settle(60)
|
||||
ev("GAME.status = ''; GAME.statusT = 0"); n_toasts = ev("$('toasts').children.length")
|
||||
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150); pg.keyboard.press('f'); settle(150)
|
||||
check('F on an ingot mold says nothing', ev('GAME.status') == '' and ev("$('toasts').children.length") == n_toasts, ev('GAME.status'))
|
||||
check('F on an automatic ingot mold opens vacuum menu without a toast', ev("!$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts)
|
||||
ev("closeModal('m-dialog')")
|
||||
check('F with vacuum uses what is in reach before opening the menu', ev("""(() => {
|
||||
const old = interactAt; interactAt = () => 'Gate opened.';
|
||||
try { doInteract(); } finally { interactAt = old; }
|
||||
return $('m-dialog').hidden && GAME.status === 'Gate opened.';
|
||||
})()"""))
|
||||
settle(1500)
|
||||
pg.screenshot(path=str(out / '06-bars.png'))
|
||||
pg.keyboard.press('2'); settle(60)
|
||||
@@ -724,10 +725,8 @@ with sync_playwright() as p:
|
||||
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
|
||||
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
|
||||
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
|
||||
ev("GAME.collect.clear(); setPickMode('first'); GAME.aim = { x: Math.floor(bot.x), y: Math.floor(bot.y) }; setCell(Math.floor(bot.y)*W+Math.floor(bot.x), COAL, 20, 0); __pad.buttons[2].pressed = true; pollPad(); __pad.buttons[2].pressed = false; pollPad()")
|
||||
check('gamepad X keeps R pick-up marking', ev("GAME.collect.has(COAL) && GAME.pickMode === 'marked'"))
|
||||
ev("__pad.buttons[1].pressed = true; pollPad(); __pad.buttons[1].pressed = false; pollPad()")
|
||||
check('gamepad B cycles pick-up mode', ev("GAME.pickMode === 'all'"))
|
||||
ev("GAME.collect.clear(); GAME.aim = { x: Math.floor(bot.x), y: Math.floor(bot.y) }; setCell(Math.floor(bot.y)*W+Math.floor(bot.x), COAL, 20, 0); __pad.buttons[2].pressed = true; pollPad(); __pad.buttons[2].pressed = false; pollPad()")
|
||||
check('gamepad X keeps R pick-up marking', ev("GAME.collect.has(COAL)"))
|
||||
ev("__pad.buttons[15].pressed = true; pollPad(); __pad.buttons[15].pressed = false; pollPad()")
|
||||
check('gamepad d-pad chooses a single blow material', ev('GAME.blow.size === 1'))
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ ticks(run, 30);
|
||||
const got = run('toolVacuum(120, 104, 6, m => m === RSTONE).got');
|
||||
check('vacuum picks only the filtered material', run('bag[RSTONE]') > 0 && run('bag[COAL]') === 0, `${run('bag[RSTONE]')} rubble, ${run('bag[COAL]')} coal`);
|
||||
run('toolVacuum(102, 98, 4, m => true)');
|
||||
check('vacuum everything mode picks coal', run('bag[COAL]') > 0, run('bag[COAL]'));
|
||||
check('unfiltered vacuum picks coal', run('bag[COAL]') > 0, run('bag[COAL]'));
|
||||
// 3. drop: grains fall
|
||||
const c0 = run('bag[COAL]');
|
||||
run('bot.x = 100; bot.y = 60;');
|
||||
|
||||
Reference in New Issue
Block a user