Vacuum: pick up what you click, blow out what's in hand (t20)
A drag takes only the material it starts on (Shift: everything). The blow selection is the hand: click or Q/E for one material, Ctrl+click for a mix. Empty materials leave the hand, an empty hand blows nothing, and a pickup fills an empty hand. Removes the pick-up marks and the F vacuum menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+12
-2
@@ -43,7 +43,7 @@ decides something new, add it under "Decisions" with the date.
|
|||||||
## The core loop
|
## The core loop
|
||||||
|
|
||||||
1. Dig with the resonance drill. Rock shatters into loose grains: rubble, ore, coal, clay, sand.
|
1. Dig with the resonance drill. Rock shatters into loose grains: rubble, ore, coal, clay, sand.
|
||||||
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.
|
2. Vacuum up what you want (a drag takes only the material it starts on), and blow out what's in hand where you need it.
|
||||||
3. Build a furnace from wall blocks, with a grate floor over a mold. Charge it with ore and coal,
|
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.
|
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.
|
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.
|
||||||
@@ -128,7 +128,8 @@ above, including:
|
|||||||
- **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 marked materials** once any are marked, even if the drag starts on
|
- **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.
|
something else. The t6 controls below replace the shared ticks; t16 replaces t6's modes; t20
|
||||||
|
replaces the marks with pick-what-you-click.
|
||||||
- **The Refiner** grinds stone, granite and basalt rubble, gravel, sand and dirt for trace ore,
|
- **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.
|
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
|
- **Pipes, pumps and outlets** move liquids up to 500°C, one liquid per line. A line is stored as
|
||||||
@@ -234,6 +235,15 @@ above, including:
|
|||||||
the bag; Ctrl+click a bag name adds it to the blow mix and Ctrl+right-click to pick-up; the menu
|
the bag; Ctrl+click a bag name adds it to the blow mix and Ctrl+right-click to pick-up; the menu
|
||||||
is one line per material because the first version was too large.
|
is one line per material because the first version was too large.
|
||||||
|
|
||||||
|
- **Pick up what you click, blow out what's in hand (t20, 2026-10-24).** Replaces t16's pick-up
|
||||||
|
marks and the vacuum menu; the owner wanted something simpler and more intuitive. A left drag
|
||||||
|
takes only the material it starts on (or the first loose grain it reaches); Shift takes
|
||||||
|
everything. The hand is the blow selection: click a bag name or Q/E for one material, Ctrl+click
|
||||||
|
for a mix, Shift + right click for everything loose. When something in hand runs out it leaves
|
||||||
|
the hand (this settles the old t18 question), and an empty hand blows nothing and says so.
|
||||||
|
Picking something up fills an empty hand, never a chosen one. One tool, no menu; R and F do
|
||||||
|
nothing special with the Vacuum. The owner mostly uses it to collect ore and fuel and feed furnaces.
|
||||||
|
|
||||||
- **Fog of war also reveals the ground around the bot (t17, 2026-10-24).** About 20 grains in every
|
- **Fog of war also reveals the ground around the bot (t17, 2026-10-24).** About 20 grains in every
|
||||||
direction, through rock, fully clear with a soft edge. Line of sight alone hid everything inside
|
direction, through rock, fully clear with a soft edge. Line of sight alone hid everything inside
|
||||||
the ground, so the owner couldn't see what was around them or where to dig.
|
the ground, so the owner couldn't see what was around them or where to dig.
|
||||||
|
|||||||
+8
-8
@@ -288,7 +288,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
|
|||||||
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
||||||
- variables: treeBases, saplings, treeDraw
|
- variables: treeBases, saplings, treeDraw
|
||||||
|
|
||||||
### src/sim/vacuum.js (165 lines)
|
### src/sim/vacuum.js (166 lines)
|
||||||
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
||||||
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
||||||
- variables: blowRR, FLY_G, FLY_Q
|
- variables: blowRR, FLY_G, FLY_Q
|
||||||
@@ -366,9 +366,9 @@ Audio: chiptune effects and generative music that changes with depth. Everything
|
|||||||
- functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler
|
- functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler
|
||||||
- variables: AU, SFX, SFX_NEAR, SFX_FAR, ZONES
|
- variables: AU, SFX, SFX_NEAR, SFX_FAR, ZONES
|
||||||
|
|
||||||
### src/ui/state.js (67 lines)
|
### src/ui/state.js (66 lines)
|
||||||
Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line.
|
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
|
- functions: loadSettings, saveSettings, el, esc, rgbOf, fmtT, toast, setStatus
|
||||||
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
|
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
|
||||||
|
|
||||||
### src/ui/title.js (46 lines)
|
### src/ui/title.js (46 lines)
|
||||||
@@ -395,18 +395,18 @@ The notebook (N): milestones, market, wallet, blueprints, controls.
|
|||||||
- functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml
|
- functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml
|
||||||
- variables: nbTab, MARKET_GROUPS
|
- variables: nbTab, MARKET_GROUPS
|
||||||
|
|
||||||
### src/ui/hud.js (40 lines)
|
### src/ui/hud.js (34 lines)
|
||||||
The tool hotbar and what is selected.
|
The tool hotbar and what is selected.
|
||||||
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, ensureBlow, piece
|
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
|
||||||
|
|
||||||
### src/ui/build-menu.js (102 lines)
|
### src/ui/build-menu.js (102 lines)
|
||||||
The build menu along the bottom: categories, piece tiles and their icons.
|
The build menu along the bottom: categories, piece tiles and their icons.
|
||||||
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
|
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
|
||||||
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
|
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
|
||||||
|
|
||||||
### src/ui/selection.js (276 lines)
|
### src/ui/selection.js (230 lines)
|
||||||
The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
|
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, toggleCollect, toggleBlow, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openVacuumMenu, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
||||||
- variables: lastBagKey
|
- variables: lastBagKey
|
||||||
|
|
||||||
### src/ui/info.js (171 lines)
|
### src/ui/info.js (171 lines)
|
||||||
@@ -418,7 +418,7 @@ Keyboard and mouse listeners.
|
|||||||
- functions: isTyping
|
- functions: isTyping
|
||||||
- variables: canvas
|
- variables: canvas
|
||||||
|
|
||||||
### src/ui/actions.js (233 lines)
|
### src/ui/actions.js (237 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
|
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
|
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
||||||
- variables: ovShown
|
- variables: ovShown
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ 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-24: t20: pick up what you click, blow out what's in hand (Claude Code, Codex)
|
||||||
|
|
||||||
|
- After discussing options with the owner: one Vacuum, a drag locks onto the material it starts on
|
||||||
|
(Shift: everything), and `GAME.blow` is "the hand" (click/Q/E, Ctrl+click mix). Empty materials
|
||||||
|
leave the hand; an empty hand blows nothing; a pickup fills an empty hand. Removed `GAME.collect`,
|
||||||
|
R marks, Ctrl+right-click, `openVacuumMenu`, `ensureBlow` and the `.vpick` styles.
|
||||||
|
- Files: `src/ui/{actions,selection,hud,state,previews,build-menu,info,notebook,session}.js`,
|
||||||
|
`src/sim/{vacuum,progress}.js` (`toolVacuum` returns the first material picked up),
|
||||||
|
`src/styles/style.css`, `test/play.py`, `test/t_basics.js`.
|
||||||
|
- Tests: `run_all.py --browser` all passed (play.py 214 checks). Screenshots `shots/t20-*.png`
|
||||||
|
(empty hand, one item, mix, 900px).
|
||||||
|
|
||||||
## 2026-10-24: t17 follow-up: no jagged rim, unexplored is black (Claude Code)
|
## 2026-10-24: t17 follow-up: no jagged rim, unexplored is black (Claude Code)
|
||||||
|
|
||||||
- The owner still saw a jagged grey edge. Causes found by sampling pixels: the fade went to 0
|
- The owner still saw a jagged grey edge. Causes found by sampling pixels: the fade went to 0
|
||||||
|
|||||||
+2
-2
@@ -22,12 +22,12 @@ const STARTER_DESIGNS = [
|
|||||||
|
|
||||||
// ---------- Milestones ----------
|
// ---------- Milestones ----------
|
||||||
const MILESTONES = [
|
const MILESTONES = [
|
||||||
{ 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: 'dig', title: 'Break ground', msg: 'Broken material falls loose. The Vacuum (2) picks up what you click. Click or Q/E in the bag chooses the hand; Ctrl+click mixes, Shift takes or blows everything.', 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: '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: '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: '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: '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: in the vacuum menu (F), mark coal and 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: Ctrl+click coal and copper ore in the bag to blow a mix. 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: '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: '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.' },
|
{ 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.' },
|
||||||
|
|||||||
+6
-5
@@ -11,11 +11,11 @@ function vacuumTargetAt(cx, cy, r) {
|
|||||||
}
|
}
|
||||||
// Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true.
|
// Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true.
|
||||||
function toolVacuum(cx, cy, r, pick) {
|
function toolVacuum(cx, cy, r, pick) {
|
||||||
let got = 0, full = false, budget = 48;
|
let got = 0, first = 0, full = false, budget = 48;
|
||||||
for (const e of entsInCircle(cx, cy, r)) {
|
for (const e of entsInCircle(cx, cy, r)) {
|
||||||
if (!pick(e.m) || !inReach(e.ix + 2, e.iy + 1)) continue;
|
if (!pick(e.m) || !inReach(e.ix + 2, e.iy + 1)) continue;
|
||||||
if (bagTotal >= bagCap()) { full = true; break; }
|
if (bagTotal >= bagCap()) { full = true; break; }
|
||||||
bagAdd(e.m, 1); removeEnt(e); got++;
|
bagAdd(e.m, 1); removeEnt(e); got++; if (!first) first = e.m;
|
||||||
emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m });
|
emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m });
|
||||||
}
|
}
|
||||||
if (!full) forBrush(cx, cy, r, (x, y, i) => {
|
if (!full) forBrush(cx, cy, r, (x, y, i) => {
|
||||||
@@ -24,11 +24,12 @@ function toolVacuum(cx, cy, r, pick) {
|
|||||||
if (!pick(m)) return;
|
if (!pick(m)) return;
|
||||||
if (part[i] >= P_TU && part[i] <= P_TR) return;
|
if (part[i] >= P_TU && part[i] <= P_TR) return;
|
||||||
if (bagTotal >= bagCap()) { full = true; return false; }
|
if (bagTotal >= bagCap()) { full = true; return false; }
|
||||||
|
if (!first) first = m;
|
||||||
bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++;
|
bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++;
|
||||||
if (got & 1) emit({ t: 'suck', x, y, m });
|
if (got & 1) emit({ t: 'suck', x, y, m });
|
||||||
if (--budget <= 0) return false;
|
if (--budget <= 0) return false;
|
||||||
});
|
});
|
||||||
return { got, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
return { got, m: first, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
||||||
}
|
}
|
||||||
// Blow: grains leave the bot's nozzle and fly to the cursor, where they drop.
|
// Blow: grains leave the bot's nozzle and fly to the cursor, where they drop.
|
||||||
// Liquids need the Flask. Bars are thrown, one at a time.
|
// Liquids need the Flask. Bars are thrown, one at a time.
|
||||||
@@ -45,11 +46,11 @@ function toolBlow(cx, cy, r, m, mix) {
|
|||||||
let pickM = null;
|
let pickM = null;
|
||||||
if (mix && mix.length) {
|
if (mix && mix.length) {
|
||||||
const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask));
|
const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask));
|
||||||
if (!avail.length) return { placed: 0, status: 'No marked materials left to blow.' };
|
if (!avail.length) return { placed: 0, status: 'Nothing in hand: Q/E or click something in the bag.' };
|
||||||
pickM = () => { for (let k = 0; k < avail.length; k++) { const c = avail[(blowRR++) % avail.length]; if (bag[c] > 0) return c; } return 0; };
|
pickM = () => { for (let k = 0; k < avail.length; k++) { const c = avail[(blowRR++) % avail.length]; if (bag[c] > 0) return c; } return 0; };
|
||||||
m = avail[0];
|
m = avail[0];
|
||||||
}
|
}
|
||||||
if (!m) return { placed: 0, status: 'Pick what to blow out: Q/E, or click it in your bag.' };
|
if (!m) return { placed: 0, status: 'Nothing in hand: Q/E or click something in the bag.' };
|
||||||
if (bag[m] <= 0) return { placed: 0, status: `No ${MD[m].name.toLowerCase()} left.` };
|
if (bag[m] <= 0) return { placed: 0, status: `No ${MD[m].name.toLowerCase()} left.` };
|
||||||
const ph = PH[m];
|
const ph = PH[m];
|
||||||
if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' };
|
if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' };
|
||||||
|
|||||||
@@ -362,14 +362,4 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
|
|||||||
#m-dialog { z-index: 21; }
|
#m-dialog { z-index: 21; }
|
||||||
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
|
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
|
||||||
|
|
||||||
.vpick { display: grid; gap: 1px; max-height: min(56vh, 460px); overflow-y: auto; padding: 2px; }
|
|
||||||
.vrow { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto 52px 52px; align-items: center; gap: 8px; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
|
|
||||||
.vrow:not(.vhead):hover { background: var(--soot-2); }
|
|
||||||
.vrow .sw { width: 12px; height: 12px; border-radius: 2px; }
|
|
||||||
.vrow .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.vrow .num { color: var(--ash); }
|
|
||||||
.vrow.vhead { color: var(--ash-dim); white-space: nowrap; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
||||||
.vrow.vhead span:nth-child(n+4) { text-align: center; }
|
|
||||||
.vrow button { padding: 1px 0; min-height: 0; font-size: 12px; justify-content: center; }
|
|
||||||
.vrow 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; }
|
.vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; }
|
||||||
|
|||||||
+9
-5
@@ -44,7 +44,6 @@ function doInteract() {
|
|||||||
if (msg) break outer;
|
if (msg) break outer;
|
||||||
}
|
}
|
||||||
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
|
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
|
||||||
else 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
|
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
|
// Pick (middle mouse): the piece under the cursor becomes the one you build, with its
|
||||||
@@ -119,7 +118,7 @@ function primaryPress(shift) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t === 'vacuum') {
|
if (t === 'vacuum') {
|
||||||
GAME.vacFilter = shift || !GAME.collect.size ? -1 : 0;
|
GAME.vacFilter = shift ? -1 : grainAt(ax, ay);
|
||||||
hudDirty = true;
|
hudDirty = true;
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
const P = piece();
|
const P = piece();
|
||||||
@@ -205,9 +204,13 @@ function toolTick() {
|
|||||||
const r = toolPulse(ax, ay, GAME.drillW, GAME.drillTune);
|
const r = toolPulse(ax, ay, GAME.drillW, GAME.drillTune);
|
||||||
if (r.fired) { playSfx('pulse', GAME.drillW); if (r.status) setStatus(r.status); }
|
if (r.fired) { playSfx('pulse', GAME.drillW); if (r.status) setStatus(r.status); }
|
||||||
} else if (t === 'vacuum') {
|
} else if (t === 'vacuum') {
|
||||||
const r = toolVacuum(ax, ay, GAME.brush[1], GAME.vacFilter < 0 || !GAME.collect.size ? () => true : m => GAME.collect.has(m));
|
if (GAME.vacFilter === 0) { GAME.vacFilter = grainAt(ax, ay); if (GAME.vacFilter) hudDirty = true; }
|
||||||
if (r.got) { playSfx('suck'); hudDirty = true; }
|
const f = GAME.vacFilter;
|
||||||
|
if (f !== 0) {
|
||||||
|
const r = toolVacuum(ax, ay, GAME.brush[1], f < 0 ? () => true : m => m === f);
|
||||||
|
if (r.got) { if (!GAME.blow.size) { GAME.blow = new Set([f > 0 ? f : r.m]); lastBagKey = ''; } playSfx('suck'); hudDirty = true; }
|
||||||
if (r.status) setStatus(r.status);
|
if (r.status) setStatus(r.status);
|
||||||
|
}
|
||||||
} else if (t === 'ignite') {
|
} 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.'); }
|
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.');
|
else setStatus('Out of reach.');
|
||||||
@@ -215,9 +218,10 @@ function toolTick() {
|
|||||||
}
|
}
|
||||||
if (GAME.rmb && !GAME.rmbConsumed) {
|
if (GAME.rmb && !GAME.rmbConsumed) {
|
||||||
if (t === 'vacuum') {
|
if (t === 'vacuum') {
|
||||||
ensureBlow();
|
if (!GAME.blowAll && !GAME.blow.size) { setStatus('Nothing in hand: Q/E or click something in the bag.'); return; }
|
||||||
const list = GAME.blowAll ? blowAllList() : blowList();
|
const list = GAME.blowAll ? blowAllList() : blowList();
|
||||||
const r = GAME.blowAll && !list.length ? { placed: 0, status: 'Nothing loose in the bag to blow.' } : toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null);
|
const r = GAME.blowAll && !list.length ? { placed: 0, status: 'Nothing loose in the bag to blow.' } : toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null);
|
||||||
|
for (const m of GAME.blow) if (bag[m] <= 0) { GAME.blow.delete(m); lastBagKey = ''; hudDirty = true; }
|
||||||
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
||||||
|
|||||||
@@ -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`;
|
pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`;
|
||||||
}
|
}
|
||||||
function setupBuildBar() {
|
function setupBuildBar() {
|
||||||
$('selcard').addEventListener('click', e => { if (e.target.closest('[data-wall-picker]')) openWallPicker(); else if (e.target.closest('[data-vac-menu]')) openVacuumMenu(); });
|
$('selcard').addEventListener('click', e => { if (e.target.closest('[data-wall-picker]')) openWallPicker(); });
|
||||||
const bar = $('buildbar'); if (!bar) return;
|
const bar = $('buildbar'); if (!bar) return;
|
||||||
bar.addEventListener('click', e => {
|
bar.addEventListener('click', e => {
|
||||||
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
|
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
|
||||||
|
|||||||
@@ -25,16 +25,10 @@ function grainAt(ax, ay) {
|
|||||||
function selectTool(k) {
|
function selectTool(k) {
|
||||||
if (k === GAME.tool) return;
|
if (k === GAME.tool) return;
|
||||||
GAME.tool = k; GAME.drag = null; hudDirty = true; renderHotbar(); playSfx('ui');
|
GAME.tool = k; GAME.drag = null; hudDirty = true; renderHotbar(); playSfx('ui');
|
||||||
if (toolId() === 'vacuum') ensureBlow();
|
|
||||||
}
|
}
|
||||||
function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); }
|
function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); }
|
||||||
function blowList() { return [...GAME.blow]; }
|
function blowList() { return [...GAME.blow]; }
|
||||||
// Shift + right click: an even mix of everything loose in the bag (bars never mix)
|
// Shift + right click: an even mix of everything loose in the bag (bars never mix)
|
||||||
function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); }
|
function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); }
|
||||||
function blowingAll() { return toolId() === 'vacuum' && (GAME.rmb ? !GAME.rmbConsumed && GAME.blowAll : modShift()); }
|
function blowingAll() { return toolId() === 'vacuum' && (GAME.rmb ? !GAME.rmbConsumed && GAME.blowAll : modShift()); }
|
||||||
// Empty materials stay chosen until the player changes the marks.
|
|
||||||
function ensureBlow() {
|
|
||||||
if (GAME.blow.size) return;
|
|
||||||
const l = bagMaterials(); if (l.length) GAME.blow.add(l[0]);
|
|
||||||
}
|
|
||||||
function piece() { return PIECES[GAME.pieceIdx]; }
|
function piece() { return PIECES[GAME.pieceIdx]; }
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ function hoverInfo(ax, ay) {
|
|||||||
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : `too hard (${HARD[m]})`);
|
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : `too hard (${HARD[m]})`);
|
||||||
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
|
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
|
||||||
}
|
}
|
||||||
if (!bar && PH[m] === PH_POWDER) t2.push(GAME.collect.has(m) ? 'loose · marked to pick up' : 'loose');
|
if (!bar && PH[m] === PH_POWDER) t2.push('loose');
|
||||||
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
|
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
|
||||||
if (flags[i] & F_BURN) t2.push('burning');
|
if (flags[i] & F_BURN) t2.push('burning');
|
||||||
const mo = moldAt(ax, ay);
|
const mo = moldAt(ax, ay);
|
||||||
|
|||||||
+6
-6
@@ -169,19 +169,19 @@ function copyCode(code) {
|
|||||||
function controlsHtml() {
|
function controlsHtml() {
|
||||||
const kb = [
|
const kb = [
|
||||||
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
||||||
['Tools', '1 – 4'], ['Cycle what you blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'],
|
['Tools', '1 – 4'], ['Cycle what is in hand · 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'], ['Vacuum menu: what to pick up and blow', 'F (with the Vacuum, away from anything to use)'], ['Add to blow · to pick up (Vacuum)', 'Ctrl+click · Ctrl+right-click a bag name'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
['Pick up what you click (Vacuum)', 'Left drag · Shift: everything'], ['Put in hand (Vacuum)', 'Click a bag name · Q/E'], ['Add to the mix (Vacuum)', 'Ctrl+click a bag name'], ['Blow everything loose in the bag', 'Shift + right click'], ['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'],
|
['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 = [
|
||||||
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
|
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
|
||||||
['Cycle what you blow · piece', 'D-pad ← →'], ['Brush size', 'D-pad ↑ ↓'], ['Interact', 'A'], ['Variant · mark material', 'X'], ['Thermal lens', 'Y'],
|
['Cycle what is in hand · piece', 'D-pad ← →'], ['Brush size', 'D-pad ↑ ↓'], ['Interact', 'A'], ['Variant · tune drill', 'X'], ['Thermal lens', 'Y'],
|
||||||
['Cancel', 'B'], ['Zoom', 'R3'], ['Lamp', 'L3'], ['Notebook', 'View / Back'], ['Menu', 'Menu / Start'],
|
['Cancel', 'B'], ['Zoom', 'R3'], ['Lamp', 'L3'], ['Notebook', 'View / Back'], ['Menu', 'Menu / Start'],
|
||||||
];
|
];
|
||||||
const tools = [
|
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.'],
|
['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 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; Shift + right click blows an even mix of everything loose in the bag. Click a bag name or use Q/E to blow only one; it stays chosen when empty. Ctrl+click a bag name adds it to what you blow; Ctrl+right-click adds it to what you pick up.'],
|
['2 Vacuum', "Left drag picks up what you click, locking onto the first loose material it reaches. Right click blows what is in hand. Click a bag name or use Q/E to choose one; Ctrl+click adds or removes it from the mix. Bars blow alone. Empty materials leave the hand. Picking up fills an empty hand. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
|
||||||
['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.'],
|
['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.'],
|
['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.'],
|
['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">Keyboard and mouse</p>${tbl(kb)}<p class="label grouphead">Gamepad</p>${tbl(pad)}
|
||||||
<p class="label grouphead">First steps</p>
|
<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>
|
<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, 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>
|
2. With the Vacuum, left drag to pick up the loose material you click. Shift takes everything.<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>
|
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. 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>
|
4. Ctrl+click coal and copper ore in the bag to blow a mix. 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>
|
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>
|
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>
|
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>
|
||||||
|
|||||||
+1
-1
@@ -153,7 +153,7 @@ function drawPreviews() {
|
|||||||
if (t === 'vacuum') {
|
if (t === 'vacuum') {
|
||||||
// what the right button would blow, next to the cursor; what the left is sucking, while it sucks
|
// 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 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.vacFilter >= 0 && GAME.collect.size ? [...GAME.collect] : [] : blowingAll() ? blowAllList() : blowList();
|
const swatches = GAME.lmb ? GAME.vacFilter > 0 ? [GAME.vacFilter] : [] : blowingAll() ? blowAllList() : blowList();
|
||||||
swatches.slice(0, 4).forEach((m, k) => { ctx.fillStyle = rgbOf(m); ctx.fillRect(qx + k * (sz + 2), qy, sz, sz); });
|
swatches.slice(0, 4).forEach((m, k) => { ctx.fillStyle = rgbOf(m); ctx.fillRect(qx + k * (sz + 2), qy, sz, sz); });
|
||||||
if (blowing || GAME.lmb) {
|
if (blowing || GAME.lmb) {
|
||||||
// faint line from the nozzle: the path the grains take
|
// faint line from the nozzle: the path the grains take
|
||||||
|
|||||||
+11
-57
@@ -23,7 +23,7 @@ function cycleItem(d) {
|
|||||||
if (t === 'vacuum') {
|
if (t === 'vacuum') {
|
||||||
const l = bagMaterials();
|
const l = bagMaterials();
|
||||||
if (!l.length) return;
|
if (!l.length) return;
|
||||||
const k = l.indexOf(blowList()[0]);
|
const k = GAME.blow.size ? l.indexOf(blowList()[0]) : d > 0 ? -1 : 0;
|
||||||
GAME.blow = new Set([l[(k + d + l.length) % l.length]]);
|
GAME.blow = new Set([l[(k + d + l.length) % l.length]]);
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
let k = BUILD_ORDER.indexOf(piece().id); k = (k + d + BUILD_ORDER.length) % BUILD_ORDER.length;
|
let k = BUILD_ORDER.indexOf(piece().id); k = (k + d + BUILD_ORDER.length) % BUILD_ORDER.length;
|
||||||
@@ -48,18 +48,10 @@ function rotate() {
|
|||||||
if (pieceUsesWall(P)) cycleWall(1);
|
if (pieceUsesWall(P)) cycleWall(1);
|
||||||
else if (P.dirs) GAME.pieceDir[P.id] = ((GAME.pieceDir[P.id] || 0) + 1) % P.dirs.length;
|
else if (P.dirs) GAME.pieceDir[P.id] = ((GAME.pieceDir[P.id] || 0) + 1) % P.dirs.length;
|
||||||
else return;
|
else return;
|
||||||
} else if (t === 'vacuum') { toggleCollectUnderCursor(); return; }
|
} else if (t === 'drill') { tuneUnderCursor(); return; }
|
||||||
else if (t === 'drill') { tuneUnderCursor(); return; }
|
|
||||||
else return;
|
else return;
|
||||||
playSfx('ui'); hudDirty = true;
|
playSfx('ui'); hudDirty = true;
|
||||||
}
|
}
|
||||||
function toggleCollect(m) {
|
|
||||||
if (!m) return;
|
|
||||||
const add = !GAME.collect.has(m);
|
|
||||||
if (add) GAME.collect.add(m); else GAME.collect.delete(m);
|
|
||||||
try { localStorage.setItem('deepgrain.collect', JSON.stringify([...GAME.collect])); } catch (e) { /* storage unavailable */ }
|
|
||||||
lastBagKey = ''; hudDirty = true; playSfx('ui');
|
|
||||||
}
|
|
||||||
function toggleBlow(m) {
|
function toggleBlow(m) {
|
||||||
if (GAME.blow.has(m)) GAME.blow.delete(m);
|
if (GAME.blow.has(m)) GAME.blow.delete(m);
|
||||||
else if (PH[m] === PH_ITEM) GAME.blow = new Set([m]);
|
else if (PH[m] === PH_ITEM) GAME.blow = new Set([m]);
|
||||||
@@ -80,11 +72,6 @@ function toggleFilterMat(m) {
|
|||||||
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
||||||
setStatus(`New filter selection updated.`, 'info');
|
setStatus(`New filter selection updated.`, 'info');
|
||||||
}
|
}
|
||||||
function toggleCollectUnderCursor() {
|
|
||||||
const m = grainAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
|
||||||
if (!m) { setStatus('Aim at something loose to mark it.', 'info'); return; }
|
|
||||||
toggleCollect(m);
|
|
||||||
}
|
|
||||||
function setBrush(d) {
|
function setBrush(d) {
|
||||||
const t = toolId();
|
const t = toolId();
|
||||||
if (t === 'drill') { setDrillWidth(GAME.drillW + d); return; }
|
if (t === 'drill') { setDrillWidth(GAME.drillW + d); return; }
|
||||||
@@ -141,37 +128,6 @@ 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(); });
|
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();
|
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"><div class="vrow vhead"><span></span><span></span><span></span><span>Pick up</span><span>Blow</span></div>${list.map(m => {
|
|
||||||
const n = esc(MD[m].name);
|
|
||||||
return `<div class="vrow"><span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${n}</span><span class="num">${bag[m] ? bag[m].toLocaleString('en-US') : 'none'}</span><button data-vm-pick="${m}" aria-pressed="${GAME.collect.has(m)}" aria-label="Pick up ${n}" title="Pick up ${n}" class="btn${GAME.collect.has(m) ? ' on' : ''}">⬇</button><button data-vm-blow="${m}" aria-pressed="${GAME.blow.has(m)}" aria-label="Blow out ${n}" title="Blow out ${n}" class="btn${GAME.blow.has(m) ? ' on' : ''}"${!bag[m] && !GAME.blow.has(m) ? ' disabled' : ''}>⬆</button></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() {
|
function openWallPicker() {
|
||||||
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
|
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]));
|
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
|
||||||
@@ -228,13 +184,12 @@ function renderSelCard() {
|
|||||||
+ '<div class="sub">Place pieces from the menu below. Drag for a line, <kbd>Shift</kbd>-drag for a box; out of reach becomes ghosts.</div>'
|
+ '<div class="sub">Place pieces from the menu below. Drag for a line, <kbd>Shift</kbd>-drag for a box; out of reach becomes ghosts.</div>'
|
||||||
+ '<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>';
|
+ '<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') {
|
} else if (t === 'vacuum') {
|
||||||
ensureBlow();
|
const all = blowingAll(), list = all ? blowAllList() : blowList();
|
||||||
const all = blowingAll(), list = all ? blowAllList() : 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 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);
|
const count = list.reduce((n, m) => n + bag[m], 0);
|
||||||
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>
|
h = `<div class="row vacrow"><b>Pick up</b><span>What you click · Shift: everything</span>${sws(GAME.lmb && GAME.vacFilter > 0 ? [GAME.vacFilter] : [])}</div>
|
||||||
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><button class="name grow vacchoice" data-vac-menu>${all ? '' : sws(list)}${all ? list.length ? `Everything · mix of ${list.length}` : 'Everything · nothing loose' : 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="row vacrow"><b>In hand</b><kbd>Q</kbd><span class="name grow vacchoice">${all ? '' : sws(list)}${all ? list.length ? `Everything · mix of ${list.length}` : 'Everything · nothing loose' : list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : 'empty'}</span><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none'}</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 or blows everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
|
<div class="sub">Left picks up what you click, right blows out what's in hand. Shift: everything. Click or Q/E in the bag to change the hand. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
|
||||||
} else if (t === 'ignite') {
|
} else if (t === 'ignite') {
|
||||||
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
|
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
|
||||||
}
|
}
|
||||||
@@ -247,9 +202,9 @@ function renderBag() {
|
|||||||
$('bagcap').textContent = `${bagTotal.toLocaleString('en-US')} / ${cap.toLocaleString('en-US')}`;
|
$('bagcap').textContent = `${bagTotal.toLocaleString('en-US')} / ${cap.toLocaleString('en-US')}`;
|
||||||
$('bagbar').firstChild.style.width = pct + '%';
|
$('bagbar').firstChild.style.width = pct + '%';
|
||||||
$('bagbar').classList.toggle('full', bagTotal >= cap);
|
$('bagbar').classList.toggle('full', bagTotal >= cap);
|
||||||
const l = BAG_ORDER.filter(m => bag[m] > 0 || GAME.collect.has(m) || GAME.blow.has(m));
|
const l = BAG_ORDER.filter(m => bag[m] > 0 || GAME.blow.has(m));
|
||||||
const t = toolId(), all = blowingAll(), blowing = all ? blowAllList() : blowList();
|
const t = toolId(), all = blowingAll(), blowing = all ? blowAllList() : blowList();
|
||||||
const key = l.map(m => m + ':' + bag[m] + (GAME.collect.has(m) ? 'c' : '') + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx + '|' + all;
|
const key = l.map(m => m + ':' + bag[m] + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx + '|' + all;
|
||||||
if (key === lastBagKey) return;
|
if (key === lastBagKey) return;
|
||||||
lastBagKey = key;
|
lastBagKey = key;
|
||||||
const box = $('baglist'); box.innerHTML = '';
|
const box = $('baglist'); box.innerHTML = '';
|
||||||
@@ -257,11 +212,10 @@ function renderBag() {
|
|||||||
for (const m of l) {
|
for (const m of l) {
|
||||||
const sel = (t === 'vacuum' && blowing.includes(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
|
const sel = (t === 'vacuum' && blowing.includes(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' : '') });
|
const row = el('div', { class: 'bagrow' + (sel ? ' on' : '') });
|
||||||
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}` });
|
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.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>`;
|
||||||
// Ctrl+click adds to (or removes from) what you blow; Ctrl+right-click, what you pick up
|
// Ctrl+click adds to or removes from the hand.
|
||||||
b.oncontextmenu = e => { e.preventDefault(); if (!(e.ctrlKey || e.metaKey)) return; toggleCollect(m); if (t !== 'vacuum') selectTool(1); };
|
b.oncontextmenu = e => e.preventDefault();
|
||||||
b.onclick = e => {
|
b.onclick = e => {
|
||||||
if (e.ctrlKey || e.metaKey) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
|
if (e.ctrlKey || e.metaKey) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
|
||||||
if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; }
|
if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; }
|
||||||
@@ -272,5 +226,5 @@ function renderBag() {
|
|||||||
row.appendChild(b);
|
row.appendChild(b);
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
}
|
}
|
||||||
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'Click: blow only this · Ctrl+click: add to blow · Ctrl+right-click: add to pick up · F: vacuum menu'));
|
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'Click: put in hand · Ctrl+click: add to the mix · Q/E: next'));
|
||||||
}
|
}
|
||||||
|
|||||||
+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;
|
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);
|
showLoading(false);
|
||||||
enterGame();
|
enterGame();
|
||||||
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 });
|
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Vacuum (2) picks up what you click. Right click blows what is in hand. Click or Q/E in the bag chooses the hand; Ctrl+click mixes, Shift takes or blows everything.', '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');
|
await saveWorld('auto');
|
||||||
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
||||||
}, 30);
|
}, 30);
|
||||||
|
|||||||
+1
-2
@@ -36,10 +36,9 @@ const ICONS = {
|
|||||||
};
|
};
|
||||||
const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN, BARGL, BARSB, AUORE, AGORE, ZNORE, SNORE, RSTONE, LCLAY, RBRICK, LDIRT, LWOOD, ACORN, COAL, CHARCOAL, CUORE, FEORE, SAND, GRAVEL, RGRAN, RBAS, CULLET,
|
const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN, BARGL, BARSB, AUORE, AGORE, ZNORE, SNORE, RSTONE, LCLAY, RBRICK, LDIRT, LWOOD, ACORN, COAL, CHARCOAL, CUORE, FEORE, SAND, GRAVEL, RGRAN, RBAS, CULLET,
|
||||||
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
|
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
|
||||||
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 = {
|
const GAME = {
|
||||||
running: false, paused: false, tool: 0, blow: new Set(), wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
|
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, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
|
brush: [3, 2, 0, 0, 0], 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 },
|
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 },
|
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: '',
|
lastAutosave: 0, playTime: 0, lastHud: 0, worldId: '', worldName: '',
|
||||||
|
|||||||
+52
-76
@@ -21,11 +21,6 @@ with sync_playwright() as p:
|
|||||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
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.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)
|
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
|
||||||
pg.evaluate("toggleCollect(CUORE)")
|
|
||||||
pg.reload(); pg.wait_for_timeout(300)
|
|
||||||
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 () => {
|
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'); }); }
|
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
|
||||||
catch (e) { return await DB.get('abort-test') === undefined; }
|
catch (e) { return await DB.get('abort-test') === undefined; }
|
||||||
@@ -220,7 +215,7 @@ with sync_playwright() as p:
|
|||||||
pg.screenshot(path=str(out / '01-dug.png'))
|
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; })()'))
|
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: everything unless marked
|
# 2. vacuum, left button: lock onto what you click
|
||||||
pg.keyboard.press('2'); settle(100)
|
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++) {{
|
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; }})()''')
|
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; }})()''')
|
||||||
@@ -232,15 +227,35 @@ with sync_playwright() as p:
|
|||||||
got = ev(f'bag[{tgt[2]}]')
|
got = ev(f'bag[{tgt[2]}]')
|
||||||
others = ev(f'bagTotal - 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) picks the clicked material', got > 0, f"{ev(f'MD[{tgt[2]}].name')} x{got}")
|
||||||
check('unmarked vacuum takes both materials in a mixed pile', ev("""(() => {
|
# Drive one tick at a time so the pile cannot fall between the click and pickup.
|
||||||
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, cells = [], dirt = bag[LDIRT], stone = bag[RSTONE];
|
ev("""window.vacCase = (mode, hand = []) => {
|
||||||
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 x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, cells = [], aim = GAME.aim, blow = GAME.blow;
|
||||||
const aim = GAME.aim; GAME.aim = { x, y }; GAME.lmb = true; primaryPress(false); toolTick();
|
const oldBag = bag.slice(), total = bagTotal, flyers0 = flyers.length;
|
||||||
const ok = GAME.vacFilter === -1 && bag[LDIRT] > dirt && bag[RSTONE] > stone;
|
for (let dy = -6; dy <= 6; dy++) for (let dx = -6; dx <= 6; dx++) { const i = (y+dy)*W+x+dx; cells.push([i, mat[i], temp[i], flags[i]]); setCell(i, EMPTY, 20, 0); }
|
||||||
primaryRelease(false); GAME.lmb = false; GAME.aim = aim;
|
bag.fill(0); bagTotal = 0; GAME.blow = new Set(hand); GAME.aim = { x, y }; GAME.lmb = true;
|
||||||
|
if (mode !== 'air') setCell(y*W+x+1, LDIRT, 20, 0);
|
||||||
|
if (mode !== 'air') setCell(y*W+x, CUORE, 20, 0);
|
||||||
|
primaryPress(mode === 'shift');
|
||||||
|
let waiting = true;
|
||||||
|
if (mode === 'air') { toolTick(); waiting = GAME.vacFilter === 0 && bagTotal === 0; setCell(y*W+x, CUORE, 20, 0); setCell(y*W+x+1, LDIRT, 20, 0); }
|
||||||
|
toolTick();
|
||||||
|
if (mode === 'ore') { GAME.aim = { x: x + 1, y }; toolTick(); }
|
||||||
|
const result = { ore: bag[CUORE], dirt: bag[LDIRT], dirtStays: mat[y*W+x+1] === LDIRT, filter: GAME.vacFilter, hand: [...GAME.blow], waiting };
|
||||||
|
primaryRelease(false); GAME.lmb = false; GAME.aim = aim; GAME.blow = blow;
|
||||||
for (const [i, m, t, f] of cells) setCell(i, m, t, f);
|
for (const [i, m, t, f] of cells) setCell(i, m, t, f);
|
||||||
bagTake(LDIRT, bag[LDIRT] - dirt); bagTake(RSTONE, bag[RSTONE] - stone); return ok;
|
bag.set(oldBag); bagTotal = total; flyers.length = flyers0; return result;
|
||||||
})()"""))
|
}""")
|
||||||
|
r = ev("vacCase('ore')")
|
||||||
|
check('drag starting on ore takes only ore and leaves dirt', r['ore'] > 0 and r['dirt'] == 0 and r['dirtStays'] and r['filter'] == ev('CUORE'))
|
||||||
|
check('pickup puts ore in an empty hand', r['hand'] == [ev('CUORE')])
|
||||||
|
r = ev("vacCase('air')")
|
||||||
|
check('drag starting in air waits and locks onto the first loose grain', r['waiting'] and r['ore'] > 0 and r['dirt'] == 0 and r['filter'] == ev('CUORE'))
|
||||||
|
r = ev("vacCase('shift')")
|
||||||
|
check('Shift drag takes everything and puts the first pickup in hand', r['ore'] > 0 and r['dirt'] > 0 and r['filter'] == -1 and r['hand'] == [ev('CUORE')])
|
||||||
|
check('picking up ore keeps coal in hand', ev("vacCase('ore', [COAL]).hand.join() === String(COAL)"))
|
||||||
|
check('picking up ore keeps a chosen mix', ev("vacCase('ore', [COAL, RSTONE]).hand.join() === [COAL, RSTONE].join()"))
|
||||||
|
check('selecting Vacuum leaves an empty hand empty', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); selectTool(0); selectTool(1); renderSelCard(); const ok = !GAME.blow.size && $('selcard').querySelector('.vacchoice').textContent === 'empty' && $('selcard').querySelector('.num.chip').textContent === 'none'; GAME.blow = old; return ok; })()"))
|
||||||
|
check('Q/E from an empty hand chooses the first or last bag material', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); cycleItem(1); const first = blowList()[0] === bagMaterials()[0]; GAME.blow.clear(); cycleItem(-1); const last = blowList()[0] === bagMaterials().at(-1); GAME.blow = old; return first && last; })()"))
|
||||||
check('middle click chooses a loose material even with none in the bag', ev("""(() => {
|
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;
|
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, i = y*W+x;
|
||||||
const old = [mat[i], temp[i], flags[i]], aim = GAME.aim, blow = GAME.blow, n = bag[LDIRT];
|
const old = [mat[i], temp[i], flags[i]], aim = GAME.aim, blow = GAME.blow, n = bag[LDIRT];
|
||||||
@@ -248,21 +263,9 @@ with sync_playwright() as p:
|
|||||||
const ok = GAME.blow.size === 1 && GAME.blow.has(LDIRT);
|
const ok = GAME.blow.size === 1 && GAME.blow.has(LDIRT);
|
||||||
setCell(i, ...old); bag[LDIRT] = n; GAME.aim = aim; GAME.blow = blow; return ok;
|
setCell(i, ...old); bag[LDIRT] = n; GAME.aim = aim; GAME.blow = blow; return ok;
|
||||||
})()"""))
|
})()"""))
|
||||||
# R marks pick-up without changing the blow choice
|
|
||||||
blow_before = ev('[...GAME.blow]')
|
|
||||||
tgt2 = ev(f'''(() => {{ for (let r = 0; r < 40; 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; }})()''')
|
|
||||||
check('a loose grain is available for R marking', tgt2 is not None)
|
|
||||||
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 without changing blow', ev(f"GAME.collect.has({tgt2[2]})") and ev('[...GAME.blow]') == blow_before)
|
|
||||||
ev('renderBag()'); settle(50)
|
|
||||||
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'))
|
pg.screenshot(path=str(out / '02-vacuum.png'))
|
||||||
|
|
||||||
# 2b. the vacuum filter is fixed when you click: ore with stone around it, ore marked
|
# 2b. the vacuum filter stays fixed while dragging through a mixed pile
|
||||||
def mixed_pile(x0, y0):
|
def mixed_pile(x0, y0):
|
||||||
ev(f"{{ const r = mulberry32(3); for (let y = {y0}; y < {y0} + 6; y++) for (let x = {x0}; x < {x0} + 14; x++) setCell(y*W+x, r() < 0.5 ? CUORE : RSTONE, AMB[y], 0); }}")
|
ev(f"{{ const r = mulberry32(3); for (let y = {y0}; y < {y0} + 6; y++) for (let x = {x0}; x < {x0} + 14; x++) setCell(y*W+x, r() < 0.5 ? CUORE : RSTONE, AMB[y], 0); }}")
|
||||||
def suck_at(x, y, ms=900, shift=False, fly=None):
|
def suck_at(x, y, ms=900, shift=False, fly=None):
|
||||||
@@ -283,14 +286,12 @@ with sync_playwright() as p:
|
|||||||
mixed_pile(px0, py0); settle(400)
|
mixed_pile(px0, py0); settle(400)
|
||||||
ore = find_ore()
|
ore = find_ore()
|
||||||
refill('bag.fill(0)')
|
refill('bag.fill(0)')
|
||||||
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
|
|
||||||
suck_at(*ore)
|
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]')}")
|
check('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)')
|
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; }})()')
|
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)
|
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]')}")
|
check('starting on stone rubble takes only stone', ev('bag[RSTONE]') > 0 and ev('bag[CUORE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||||||
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
|
|
||||||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||||||
ore = find_ore()
|
ore = find_ore()
|
||||||
suck_at(*ore, ms=500, shift=True, fly='d')
|
suck_at(*ore, ms=500, shift=True, fly='d')
|
||||||
@@ -300,10 +301,9 @@ with sync_playwright() as p:
|
|||||||
ore = find_ore()
|
ore = find_ore()
|
||||||
suck_at(*ore, ms=500, shift=True)
|
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]')}")
|
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]')}")
|
||||||
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
|
|
||||||
ev("GAME.aim = { x: Math.floor(bot.x), y: 1 }")
|
ev("GAME.aim = { x: Math.floor(bot.x), y: 1 }")
|
||||||
pg.keyboard.press('f'); settle(100)
|
pg.keyboard.press('f'); settle(100)
|
||||||
check('F away from usable things opens Vacuum', ev("!$('m-dialog').hidden && $('dlg-title').textContent === 'Vacuum'"))
|
check('F away from usable things says nothing to use', ev("$('m-dialog').hidden && GAME.status === 'Nothing to use there.'"))
|
||||||
ev("closeModal('m-dialog')")
|
ev("closeModal('m-dialog')")
|
||||||
# zoom jumps straight to the bot
|
# zoom jumps straight to the bot
|
||||||
pg.mouse.move(640, 400); settle(600)
|
pg.mouse.move(640, 400); settle(600)
|
||||||
@@ -313,7 +313,7 @@ with sync_playwright() as p:
|
|||||||
pg.mouse.wheel(0, 100); settle(300)
|
pg.mouse.wheel(0, 100); settle(300)
|
||||||
refill('bag.fill(0)')
|
refill('bag.fill(0)')
|
||||||
|
|
||||||
# 3. right button blows the chosen material out of the bot; the choice is sticky
|
# 3. right button blows what is in hand; empty materials leave it
|
||||||
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
||||||
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
||||||
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
||||||
@@ -325,11 +325,14 @@ with sync_playwright() as p:
|
|||||||
settle(350); pg.mouse.up(button='right'); settle(800)
|
settle(350); pg.mouse.up(button='right'); settle(800)
|
||||||
check('right click blows coal out of the bag onto the ground', ev('bag[COAL]') < c0 and ev('flyers.length') == 0, f"{c0}->{ev('bag[COAL]')}")
|
check('right click blows coal out of the bag onto the ground', ev('bag[COAL]') < c0 and ev('flyers.length') == 0, f"{c0}->{ev('bag[COAL]')}")
|
||||||
refill('bag[COAL] = 3')
|
refill('bag[COAL] = 3')
|
||||||
|
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||||
hold(dx_, dy_, 400, 'right'); settle(400)
|
hold(dx_, dy_, 400, 'right'); settle(400)
|
||||||
check('chosen material stays chosen after it runs out', ev('bag[COAL] === 0 && GAME.blow.size === 1 && GAME.blow.has(COAL)'), ev('[bag[COAL], ...GAME.blow]'))
|
check('blowing the last material empties the hand', ev('bag[COAL] === 0 && !GAME.blow.size'), ev('[bag[COAL], ...GAME.blow]'))
|
||||||
|
check('empty hand blows nothing and explains how to choose', ev("(() => { const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; return bagTotal === n && flyers.length === f && GAME.status === 'Nothing in hand: Q/E or click something in the bag.'; })()"))
|
||||||
|
check('a mix drops only the material that runs out', ev("(() => { const b = bag.slice(), total = bagTotal, f = flyers.length; bag.fill(0); bag[COAL] = 1; bag[CUORE] = 100; bagTotal = 101; GAME.blow = new Set([COAL, CUORE]); GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; const ok = !bag[COAL] && bag[CUORE] > 0 && GAME.blow.size === 1 && GAME.blow.has(CUORE); bag.set(b); bagTotal = total; flyers.length = f; return ok; })()"))
|
||||||
# Shift + right click blows an even mix of everything loose in the bag, without changing the choice
|
# Shift + right click blows an even mix of everything loose in the bag, without changing the choice
|
||||||
refill('bag[COAL] = 60; bag[CUORE] = 60')
|
refill('bag[COAL] = 60; bag[CUORE] = 60')
|
||||||
ev('GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
|
ev('GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
|
||||||
pg.keyboard.down('Shift'); settle(150)
|
pg.keyboard.down('Shift'); settle(150)
|
||||||
check('Shift previews everything in the blow card', 'Everything · mix of' in pg.inner_text('#selcard'))
|
check('Shift previews everything in the blow card', 'Everything · mix of' in pg.inner_text('#selcard'))
|
||||||
check('Shift lights every loose bag row', ev("blowAllList().every(m => [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === MD[m].name))"))
|
check('Shift lights every loose bag row', ev("blowAllList().every(m => [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === MD[m].name))"))
|
||||||
@@ -353,59 +356,31 @@ with sync_playwright() as p:
|
|||||||
check('Ctrl+click adds a bag item to the blow mix', ev('GAME.blow.size === 2 && GAME.blow.has(COAL) && GAME.blow.has(CUORE)') and 'Mix of 2' in pg.inner_text('#selcard'))
|
check('Ctrl+click adds a bag item to the blow mix', ev('GAME.blow.size === 2 && GAME.blow.has(COAL) && GAME.blow.has(CUORE)') and 'Mix of 2' in pg.inner_text('#selcard'))
|
||||||
ore_row.click(modifiers=['Control']); settle(80)
|
ore_row.click(modifiers=['Control']); settle(80)
|
||||||
check('Ctrl+click again takes it out of the mix', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
check('Ctrl+click again takes it out of the mix', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||||
ev('GAME.collect = new Set(); lastBagKey = ""; hudDirty = true'); settle(80)
|
|
||||||
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right', modifiers=['Control']); settle(80)
|
|
||||||
check('Ctrl+right-click marks a bag item for pick-up', ev('GAME.collect.size === 1 && GAME.collect.has(CUORE) && GAME.blow.size === 1'))
|
|
||||||
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right', modifiers=['Control']); settle(80)
|
|
||||||
check('Ctrl+right-click again unmarks it', ev('!GAME.collect.size'))
|
|
||||||
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80)
|
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80)
|
||||||
check('a plain right click on the bag changes nothing', ev('!GAME.collect.size && GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
check('right click on a bag row leaves the hand alone', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||||
ev('GAME.collect = new Set([COAL, CUORE]); lastBagKey = ""; hudDirty = true'); settle(80)
|
check('bag rows have no pickup tag', '⬇' not in pg.inner_text('#baglist'))
|
||||||
pick_before = ev('[...GAME.collect]')
|
check('bag hint describes the hand', pg.locator('#baglist .baghint').inner_text() == 'Click: put in hand · Ctrl+click: add to the mix · Q/E: next')
|
||||||
ev('GAME.blow = new Set([RSTONE]); hudDirty = true')
|
ore_row.click(modifiers=['Control']); settle(80)
|
||||||
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('#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 has no pick or blow buttons', pg.locator('#baglist [data-pick], #baglist [data-blow]').count() == 0)
|
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)
|
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]')
|
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_}')
|
check('right click blows an even ore/coal mix', dc > 5 and abs(dc - do_) <= 2, f'coal {dc}, ore {do_}')
|
||||||
pg.keyboard.press('e'); settle(80)
|
pg.keyboard.press('e'); settle(80)
|
||||||
check('Q/E replaces the mix with one material', ev('GAME.blow.size === 1'))
|
check('Q/E replaces the mix with one material', ev('GAME.blow.size === 1'))
|
||||||
ev("GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
|
ev("GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
|
||||||
refill('bag[COAL] = 120; bag[CUORE] = 100; bag[RSTONE] = 600')
|
refill('bag[COAL] = 120; bag[CUORE] = 100; bag[RSTONE] = 600')
|
||||||
for width, height, suffix in [(1280, 800, ''), (900, 700, '-900')]:
|
for width, height, suffix in [(1280, 800, ''), (900, 700, '-900')]:
|
||||||
pg.set_viewport_size({'width': width, 'height': height}); settle(200)
|
pg.set_viewport_size({'width': width, 'height': height}); settle(200)
|
||||||
pg.screenshot(path=str(out / f'vacuum-card{suffix}.png'))
|
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); })()"))
|
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; })()"))
|
|
||||||
check(f'vacuum menu has one line per material at {width}', ev("(() => { const rows = [...document.querySelectorAll('.vpick .vrow:not(.vhead)')]; return rows.length > 3 && rows.every(r => r.getBoundingClientRect().height <= 28); })()"))
|
|
||||||
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.set_viewport_size({'width': 1280, 'height': 800})
|
||||||
pg.keyboard.press('3'); settle(80)
|
pg.keyboard.press('3'); settle(80)
|
||||||
check('build bag has no vacuum toggles', pg.locator('#baglist .mark').count() == 0)
|
check('build bag has no vacuum toggles', pg.locator('#baglist .mark').count() == 0)
|
||||||
pg.keyboard.press('2'); settle(80)
|
pg.keyboard.press('2'); settle(80)
|
||||||
ev("GAME.blow = new Set([COAL, CUORE]); 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)'))
|
check('putting a bar in hand replaces the mix', ev('GAME.blow.size === 1 && GAME.blow.has(BARCU)'))
|
||||||
ev("toggleBlow(COAL)")
|
ev("toggleBlow(COAL)")
|
||||||
check('a grain blow mark replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
check('putting a grain in hand replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||||
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
|
|
||||||
refill('bag[COAL] = 120')
|
refill('bag[COAL] = 120')
|
||||||
|
|
||||||
# 4. igniter lights it
|
# 4. igniter lights it
|
||||||
@@ -531,9 +506,9 @@ with sync_playwright() as p:
|
|||||||
pg.keyboard.press('2'); settle(60)
|
pg.keyboard.press('2'); settle(60)
|
||||||
ev("GAME.status = ''; GAME.statusT = 0"); n_toasts = ev("$('toasts').children.length")
|
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)
|
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 automatic ingot mold opens vacuum menu without a toast', ev("!$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts)
|
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts)
|
||||||
ev("closeModal('m-dialog')")
|
ev("closeModal('m-dialog')")
|
||||||
check('F with vacuum uses what is in reach before opening the menu', ev("""(() => {
|
check('F with vacuum still uses what is in reach', ev("""(() => {
|
||||||
const old = interactAt; interactAt = () => 'Gate opened.';
|
const old = interactAt; interactAt = () => 'Gate opened.';
|
||||||
try { doInteract(); } finally { interactAt = old; }
|
try { doInteract(); } finally { interactAt = old; }
|
||||||
return $('m-dialog').hidden && GAME.status === 'Gate opened.';
|
return $('m-dialog').hidden && GAME.status === 'Gate opened.';
|
||||||
@@ -751,8 +726,9 @@ with sync_playwright() as p:
|
|||||||
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
|
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)
|
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)
|
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
|
||||||
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()")
|
ev("selectTool(2); GAME.pieceIdx = PIECE_BY_ID.belt.index; GAME.pieceDir.belt = 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)"))
|
check('gamepad X rotates with the Build tool', ev('GAME.pieceDir.belt === 1'))
|
||||||
|
ev('selectTool(1)')
|
||||||
ev("__pad.buttons[15].pressed = true; pollPad(); __pad.buttons[15].pressed = false; pollPad()")
|
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'))
|
check('gamepad d-pad chooses a single blow material', ev('GAME.blow.size === 1'))
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -14,9 +14,12 @@ ticks(run, 60);
|
|||||||
// 2. vacuum with a filter: only rubble
|
// 2. vacuum with a filter: only rubble
|
||||||
rect(run, 100, 96, 104, 99, run('COAL'));
|
rect(run, 100, 96, 104, 99, run('COAL'));
|
||||||
ticks(run, 30);
|
ticks(run, 30);
|
||||||
const got = run('toolVacuum(120, 104, 6, m => m === RSTONE).got');
|
const pickup = run('toolVacuum(120, 104, 6, m => m === RSTONE)');
|
||||||
|
const got = pickup.got;
|
||||||
|
check('vacuum reports the first material picked up', pickup.m === run('RSTONE'));
|
||||||
check('vacuum picks only the filtered material', run('bag[RSTONE]') > 0 && run('bag[COAL]') === 0, `${run('bag[RSTONE]')} rubble, ${run('bag[COAL]')} coal`);
|
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)');
|
run('toolVacuum(102, 98, 4, m => true)');
|
||||||
|
check('empty vacuum reports no first material', run('toolVacuum(10, 10, 1, () => true).m') === 0);
|
||||||
check('unfiltered vacuum 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
|
// 3. drop: grains fall
|
||||||
const c0 = run('bag[COAL]');
|
const c0 = run('bag[COAL]');
|
||||||
@@ -25,7 +28,7 @@ run('for (let k=0;k<5;k++) { toolBlow(80, 70, 1, COAL); simTick(); }');
|
|||||||
check('blown coal flies out of the bot', run('flyers.length') > 0, run('flyers.length'));
|
check('blown coal flies out of the bot', run('flyers.length') > 0, run('flyers.length'));
|
||||||
ticks(run, 120);
|
ticks(run, 120);
|
||||||
check('blown coal leaves the bag and lands on the ground', run('bag[COAL]') < c0 && run('flyers.length') === 0 && count(run, run('COAL'), 60, 80, 100, 99) > 0, `${c0}->${run('bag[COAL]')} landed ${count(run, run('COAL'), 60, 80, 100, 99)}`);
|
check('blown coal leaves the bag and lands on the ground', run('bag[COAL]') < c0 && run('flyers.length') === 0 && count(run, run('COAL'), 60, 80, 100, 99) > 0, `${c0}->${run('bag[COAL]')} landed ${count(run, run('COAL'), 60, 80, 100, 99)}`);
|
||||||
check('empty blow marks explain that nothing is left', run('toolBlow(80, 70, 1, COAL, [CUORE, BARCU]).status') === 'No marked materials left to blow.');
|
check('empty blow mix explains that nothing is in hand', run('toolBlow(80, 70, 1, COAL, [CUORE, BARCU]).status') === 'Nothing in hand: Q/E or click something in the bag.');
|
||||||
// bot flies through built walls but not through natural rock
|
// bot flies through built walls but not through natural rock
|
||||||
run('bag[RSTONE] += 64; bagTotal += 64; placeBlocks(BP_WALL, RSTONE, blockRect(30, 14, 30, 17))');
|
run('bag[RSTONE] += 64; bagTotal += 64; placeBlocks(BP_WALL, RSTONE, blockRect(30, 14, 30, 17))');
|
||||||
check('bot can overlap a built wall', !run('botOverlapSolid(121, 58)'));
|
check('bot can overlap a built wall', !run('botOverlapSolid(121, 58)'));
|
||||||
|
|||||||
Reference in New Issue
Block a user