Build menu: hover popup with full info, top-left card hidden while building

The short description row became a status row for the selected piece
(variant, Replace, ghosts, messages). Gamepad shows the selected tile's popup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 19:21:23 -04:00
co-authored by Claude Opus 5.5
parent 3e313e20eb
commit f9012384fc
9 changed files with 100 additions and 41 deletions
+4 -2
View File
@@ -7,8 +7,6 @@ and stay there until the owner has checked them in the game and deleted them.
## To do ## To do
- stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots? - stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots?
- instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information.
- dont need the short description in the build menu if we have a hover info popup.
- lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need. - lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need.
- the world still seems quite small. i dont know if thats because the bot moves so fast, the bot can mine too fast, or the grains should be smaller... i would like to make the worl seem huge to the player and give them loads of resources to mine for a long game. as is, you could mine all the resources from the map pretty quickly and end up having to start a new world and new game as a result. or maybe there is some other solution that i havent thought of yet. need to brainstorm this. - the world still seems quite small. i dont know if thats because the bot moves so fast, the bot can mine too fast, or the grains should be smaller... i would like to make the worl seem huge to the player and give them loads of resources to mine for a long game. as is, you could mine all the resources from the map pretty quickly and end up having to start a new world and new game as a result. or maybe there is some other solution that i havent thought of yet. need to brainstorm this.
- there is a sound effect that seems to happen randomly but constantly. sounds like a circuit breaker flipping on or off or something. maybe like a ruller snapping on a desk. what is it? its not new ive just been ignoring it. actually it may be a part of the background music. but it seems out of place. not sure if its the music or not. it folows the player wherever it goes. but maybe the sound effects are not local? - there is a sound effect that seems to happen randomly but constantly. sounds like a circuit breaker flipping on or off or something. maybe like a ruller snapping on a desk. what is it? its not new ive just been ignoring it. actually it may be a part of the background music. but it seems out of place. not sure if its the music or not. it folows the player wherever it goes. but maybe the sound effects are not local?
@@ -25,6 +23,10 @@ and stay there until the owner has checked them in the game and deleted them.
## Done, please check ## Done, please check
- instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information.
- (agent) Hover a tile in the build menu for a popup: icon and name, cost (red if you can't afford it), size, the full description, how to place it, and the build keys. The top-left card is hidden while the build tool is out; what it showed about the selected piece (material or direction · R, filter settings · F, Replace · Ctrl, ghosts waiting, messages like "Nothing built here to pick.") is now a row under the tiles. On a gamepad the popup shows for the selected tile. Copy, cut and paste still use the top-left card.
- dont need the short description in the build menu if we have a hover info popup.
- (agent) Removed: the line under the tiles is now the selected piece's status row instead.
- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. update: i found that its because when there is a screw tube at the end of the belt, it stops the lower grains and only the ones at the top move. the screw tube shouldnt block them, it should suck them in. - i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. update: i found that its because when there is a screw tube at the end of the belt, it stops the lower grains and only the ones at the top move. the screw tube shouldnt block them, it should suck them in.
- (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix. - (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix.
- (agent) Found it: when the front of the line backs up (your tube taking less than the belt brings, or a wall), the bottom grains were blocked but the ones above them still had air ahead, so the top slid forward over a stuck bottom forever. Now a column on a belt moves as a unit: if a grain can't move, nothing above it moves either, so a backed-up heap stands still (the belt slips under it) and is fed into the tube from its front, bottom layers included. A free-running belt still carries the whole heap. To check: blow a heap of two materials onto a belt that ends in an up tube and watch the bottom material go into the tube too. tools/scenes/belt-heap.js sets up the same thing for screenshots. - (agent) Found it: when the front of the line backs up (your tube taking less than the belt brings, or a wall), the bottom grains were blocked but the ones above them still had air ahead, so the top slid forward over a stuck bottom forever. Now a column on a belt moves as a unit: if a grain can't move, nothing above it moves either, so a backed-up heap stands still (the belt slips under it) and is fed into the tube from its front, bottom layers included. A free-running belt still carries the whole heap. To check: blow a heap of two materials onto a belt that ends in an up tube and watch the bottom material go into the tube too. tools/scenes/belt-heap.js sets up the same thing for screenshots.
+3 -1
View File
@@ -146,7 +146,9 @@ above, including:
- **Pick** (middle mouse) selects the piece under the cursor with its direction, - **Pick** (middle mouse) selects the piece under the cursor with its direction,
wall material and filter settings. G is unbound, as the owner requested. wall material and filter settings. G is unbound, as the owner requested.
- **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving, - **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving,
Liquids, Machines, Molds. The owner wants more changes (see NOTES.md). Liquids, Machines, Molds. Hover a tile for its full description, cost and placement help;
gamepad players see the selected tile’s popup. The top-left card is hidden while building,
with variants, ghosts and status kept in the bar so the world stays clear.
- **No length limit when dragging.** Blocks out of the bot's reach become ghosts that it builds - **No length limit when dragging.** Blocks out of the bot's reach become ghosts that it builds
when it gets there. when it gets there.
- **Pipes are thin:** sand, water and anything else loose passes them. Pumps and outlets are - **Pipes are thin:** sand, water and anything else loose passes them. Pumps and outlets are
+4 -4
View File
@@ -159,7 +159,7 @@ dist/, shots/ build output and screenshots (not committed)
| Material properties and descriptions | `src/sim/materials.js` (`def`) | | Material properties and descriptions | `src/sim/materials.js` (`def`) |
| Prices, orders, the dock | `src/sim/economy.js` | | Prices, orders, the dock | `src/sim/economy.js` |
| Piece costs and descriptions | `src/sim/pieces.js` (`PIECES`) | | Piece costs and descriptions | `src/sim/pieces.js` (`PIECES`) |
| Build menu tabs, tiles and icons | `src/ui/build-menu.js` | | Build menu tabs, tiles, status and hover popup | `src/ui/build-menu.js` |
| Key bindings | `src/ui/input.js` (keydown); what they do: `src/ui/actions.js` | | Key bindings | `src/ui/input.js` (keydown); what they do: `src/ui/actions.js` |
| Mouse building and dragging | `src/ui/actions.js` (`primaryPress`, `primaryRelease`, `secondaryPress`, `toolTick`) | | Mouse building and dragging | `src/ui/actions.js` (`primaryPress`, `primaryRelease`, `secondaryPress`, `toolTick`) |
| Cursor previews (ghosts, drill aim, selection box) | `src/ui/previews.js` | | Cursor previews (ghosts, drill aim, selection box) | `src/ui/previews.js` |
@@ -392,12 +392,12 @@ The notebook (N): milestones, market, wallet, blueprints, controls.
The tool hotbar and what is selected. The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, mixList, ensureDropMat, piece - functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, mixList, ensureDropMat, piece
### src/ui/build-menu.js (80 lines) ### src/ui/build-menu.js (104 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, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
### src/ui/selection.js (246 lines) ### src/ui/selection.js (228 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, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
+21
View File
@@ -4,6 +4,27 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-23: build menu hover popup, top-left card hidden while building (Claude Code, with Codex)
Notes: "mouse hover reviels a new info popup…" and "dont need the short description in the build
menu…". The owner chose to hide the top-left card entirely while building.
- `src/ui/build-menu.js`: `#bb-pop` above the hovered tile (icon, name, cost, size, power, full
description, how to place, filter help, build keys), clamped to the window; on a gamepad it
shows for the selected tile. The `.bb-tip` row became `.bb-status`: variant (material,
direction, filter) chips, Replace · Ctrl, ghosts waiting, and status messages. The bar now
updates its parts separately so hovering doesn't rebuild the tiles.
- `src/ui/selection.js`: `#selcard` is hidden for the build tool unless copying, cutting or
pasting; the build branch moved to the bar. `src/styles/style.css`: popup and status styles;
on narrow screens the bottom-right panel moves up while building.
- `src/ui/notebook.js`, `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: `play.py` 118 checks (popup on hover, switching tiles, hidden on leave, gamepad,
no `.bb-tip`, card hidden while building and back for tools 1 and 2, status in the bar; the
Ctrl replace check now reads the bar). All 11 headless suites pass. `t_smooth.py`'s
half-resolution check is still flaky under load (~4.8): 1 pass in 3 runs, as before.
- Screenshots in `shots/` (build-popup-*) at 1280×800 and 900×700 in both looks.
- Open: at 900px wide the bottom-left hover-info panel sits partly under the build bar.
## 2026-10-23: a backed-up heap on a belt no longer shears (Claude Code, with Codex) ## 2026-10-23: a backed-up heap on a belt no longer shears (Claude Code, with Codex)
Note: "belts with a big pile on top only move the grains on the top of the stack…". The owner's Note: "belts with a big pile on top only move the grains on the top of the stack…". The owner's
+13 -2
View File
@@ -61,8 +61,17 @@ canvas#game.nocursor { cursor: none; }
.bb-tile:hover { border-color: var(--soot-4); background: #2a2521; } .bb-tile:hover { border-color: var(--soot-4); background: #2a2521; }
.bb-tile.on { border-color: var(--melt); background: linear-gradient(180deg, #3a2a1d, #2a201a); box-shadow: 0 0 0 1px rgba(255, 154, 60, 0.35), 0 0 14px rgba(255, 154, 60, 0.22); } .bb-tile.on { border-color: var(--melt); background: linear-gradient(180deg, #3a2a1d, #2a201a); box-shadow: 0 0 0 1px rgba(255, 154, 60, 0.35), 0 0 14px rgba(255, 154, 60, 0.22); }
.bb-tile.on svg { color: var(--melt-hot); } .bb-tile.on svg { color: var(--melt-hot); }
#buildbar .bb-tip { font-size: 0.82em; line-height: 1.35; color: var(--ash); min-height: 1.35em; } #buildbar { position: relative; }
#buildbar .bb-tip b { color: var(--bone); font-weight: 600; } .bb-menu { display: flex; flex-direction: column; gap: 6px; }
.bb-status { display: flex; align-items: center; gap: 4px; min-height: 1.4em; overflow-x: auto; }
.bb-status .sw { flex-shrink: 0; }
#bb-pop { position: absolute; bottom: calc(100% + 8px); width: min(calc(300px * var(--ui)), calc(100vw - 32px)); box-sizing: border-box; padding: calc(10px * var(--ui)); display: flex; flex-direction: column; gap: 8px; background: var(--plate); border: 1px solid var(--soot-3); border-radius: 5px; box-shadow: 0 4px 18px #0008; color: var(--bone); pointer-events: none; }
.bb-title { display: flex; align-items: center; gap: 8px; }
.bb-title svg { width: calc(26px * var(--ui)); height: calc(26px * var(--ui)); color: var(--melt-hot); flex-shrink: 0; }
.bb-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.bb-desc { font-size: 0.88em; }
.bb-place { color: var(--ash); font-size: 0.82em; }
.bb-place b { display: block; color: var(--bone); margin-bottom: 3px; }
#ovbadges { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; } #ovbadges { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.ovbadge { --ov: 92, 186, 166; display: inline-flex; align-items: center; gap: 8px; padding: 5px 8px 5px 11px; border-radius: 999px; background: rgba(12, 13, 16, 0.86); border: 1px solid rgba(var(--ov), 0.75); color: rgb(var(--ov)); font-weight: 650; font-size: 0.8em; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35); } .ovbadge { --ov: 92, 186, 166; display: inline-flex; align-items: center; gap: 8px; padding: 5px 8px 5px 11px; border-radius: 999px; background: rgba(12, 13, 16, 0.86); border: 1px solid rgba(var(--ov), 0.75); color: rgb(var(--ov)); font-weight: 650; font-size: 0.8em; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35); }
.ovbadge.lens { --ov: 255, 160, 90; } .ovbadge.lens { --ov: 255, 160, 90; }
@@ -338,3 +347,5 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
#baglist .mixrow .pick { gap: 6px; } #baglist .mixrow .pick { gap: 6px; }
#baglist .mixsw { display: flex; gap: 1px; flex: none; } #baglist .mixsw { display: flex; gap: 1px; flex: none; }
#baglist .mixsw .sw { width: 7px; height: 14px; border-radius: 1px; } #baglist .mixsw .sw { width: 7px; height: 14px; border-radius: 1px; }
@media (max-width: 1100px) { #hud.building .br { bottom: calc(160px * var(--ui)); } }
+33 -9
View File
@@ -28,7 +28,7 @@ const PIECE_ICONS = {
}; };
function pieceIcon(id) { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${PIECE_ICONS[id] || ''}</svg>`; } function pieceIcon(id) { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${PIECE_ICONS[id] || ''}</svg>`; }
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; } function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
let lastBuildKey = ''; let lastBuildKey = '', lastBuildMenu = '';
function selectBuildCat(c) { function selectBuildCat(c) {
GAME.buildCat = c; GAME.buildCat = c;
const ids = BUILD_CATS[c][1], keep = GAME.catPick && GAME.catPick[c]; const ids = BUILD_CATS[c][1], keep = GAME.catPick && GAME.catPick[c];
@@ -43,15 +43,16 @@ function cycleBuildCat(d) {
function renderBuildBar() { function renderBuildBar() {
const bar = $('buildbar'); if (!bar) return; const bar = $('buildbar'); if (!bar) return;
const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld(); const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld();
if (bar.hidden === show) bar.hidden = !show; if (bar.hidden === show) { bar.hidden = !show; $('hud').classList.toggle('building', show); }
if (!show) { lastBuildKey = ''; return; } if (!show) { lastBuildKey = ''; return; }
const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover; const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover;
const ids = BUILD_CATS[cat][1]; const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : ''];
const status = performance.now() - GAME.statusT < 3200 ? GAME.status : '';
const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length;
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join(''); const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join('');
const key = [cat, P.id, hov, afford, GAME.wallMat, Math.round(money)].join('|'); const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.ctrl, GAME.pieceDir[P.id], !!GAME.drag, GAME.dragDir, GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), ng, ghostMissing, status, GAME.statusKind, innerWidth, innerHeight, SET.uiScale].join('|');
if (key === lastBuildKey) return; if (key === lastBuildKey) return;
lastBuildKey = key; lastBuildKey = key;
const tip = PIECE_BY_ID[hov && ids.includes(hov) ? hov : P.id];
let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('') let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('')
+ '<span class="bb-keys"><kbd>Q</kbd>/<kbd>E</kbd> cycle · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab</span></div><div class="bb-tiles">'; + '<span class="bb-keys"><kbd>Q</kbd>/<kbd>E</kbd> cycle · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab</span></div><div class="bb-tiles">';
for (const id of ids) { for (const id of ids) {
@@ -59,10 +60,33 @@ function renderBuildBar() {
const sw = Q.variants === 'wall' ? `<span class="sw" style="background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>` : ''; const sw = Q.variants === 'wall' ? `<span class="sw" style="background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>` : '';
h += `<button class="bb-tile${id === P.id ? ' on' : ''}" data-piece="${id}" aria-pressed="${id === P.id}" aria-label="${esc(Q.name)}">${sw}${pieceIcon(id)}<span class="n">${esc(Q.name)}</span><span class="c${ok ? '' : ' bad'}">${esc(costText(cost, 1) || 'free')}</span></button>`; h += `<button class="bb-tile${id === P.id ? ' on' : ''}" data-piece="${id}" aria-pressed="${id === P.id}" aria-label="${esc(Q.name)}">${sw}${pieceIcon(id)}<span class="n">${esc(Q.name)}</span><span class="c${ok ? '' : ' bad'}">${esc(costText(cost, 1) || 'free')}</span></button>`;
} }
// just the first sentence here; the card at the top left has the rest for the chosen piece h += '</div>';
const first = (tip.desc.match(/^.*?[.!?](\s|$)/) || [tip.desc])[0].trim(); if (!bar.querySelector('.bb-menu')) bar.innerHTML = '<div class="bb-menu"></div><div class="bb-status"></div><div id="bb-pop" hidden></div>';
h += `</div><div class="bb-tip"><b>${esc(tip.name)}.</b> ${esc(first)}</div>`; const menu = bar.querySelector('.bb-menu');
bar.innerHTML = h; if (lastBuildMenu !== h) { menu.innerHTML = h; lastBuildMenu = h; }
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
// while dragging a line of belts, filters or tubes, the direction is the drag's
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓' })[d] || '';
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
const fc = GAME.filterCfg;
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
const variant = pieceUsesWall(P) ? `<span class="chip hot"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R</span>`
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
: dir ? dirChip : '';
bar.querySelector('.bb-status').innerHTML = variant + (GAME.ctrl ? '<span class="chip hot">Replace · Ctrl</span>' : '')
+ (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '')
+ (status ? `<span class="chip ${GAME.statusKind === 'info' ? 'ok' : 'bad'}">${esc(status)}</span>` : '');
const pop = $('bb-pop'); pop.hidden = !tip;
if (!tip) return;
const size = tip.kind === 'machine' ? `${tip.bw}×${tip.bh} blocks` : tip.kind === 'mold' ? `${MOLDS[tip.mold].bw}×${MOLDS[tip.mold].bh} blocks` : '1 block';
pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div>
<div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${tip.kind === 'block' ? 'Drag for a line, Shift-drag for a rectangle; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> picks the material.' : ''}</div>
${tip.filter ? `<div class="bb-place">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
<div class="bb-place"><kbd>Ctrl</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>`;
const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; }
const r = tile.getBoundingClientRect(), br = bar.getBoundingClientRect(), w = pop.getBoundingClientRect().width;
pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`;
} }
function setupBuildBar() { function setupBuildBar() {
const bar = $('buildbar'); if (!bar) return; const bar = $('buildbar'); if (!bar) return;
+1 -1
View File
@@ -38,7 +38,7 @@ function renderNotebook() {
} }
body.innerHTML = `<p class="note">What you have found so far. Hardness is what your drill bit has to match. Solid rock breaks into loose rubble that you vacuum up and build with.${unknown ? ` ${unknown} more to discover.` : ''}</p><div class="tablewrap"><table class="mt"><thead><tr><th>Material</th><th>State</th><th>Hard</th><th>What it does</th><th>Notes</th></tr></thead><tbody>${rows}</tbody></table></div>`; body.innerHTML = `<p class="note">What you have found so far. Hardness is what your drill bit has to match. Solid rock breaks into loose rubble that you vacuum up and build with.${unknown ? ` ${unknown} more to discover.` : ''}</p><div class="tablewrap"><table class="mt"><thead><tr><th>Material</th><th>State</th><th>Hard</th><th>What it does</th><th>Notes</th></tr></thead><tbody>${rows}</tbody></table></div>`;
} else if (nbTab === 'Pieces') { } else if (nbTab === 'Pieces') {
let h = '<p class="note">Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Your bot flies straight through anything you build.</p><div class="cards">'; let h = '<p class="note">Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Hover a tile for its cost, full description and placement help. Your bot flies straight through anything you build.</p><div class="cards">';
for (const P of PIECES) { for (const P of PIECES) {
if (P.hidden) continue; if (P.hidden) continue;
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R picks it)` : P.code === BP_WALL ? '16 grains of rubble, lumps or chunks, or 2 bars' : costText(P.cost, 1); const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R picks it)` : P.code === BP_WALL ? '16 grains of rubble, lumps or chunks, or 2 bars' : costText(P.cost, 1);
+2 -20
View File
@@ -150,6 +150,8 @@ function renderSelCard() {
let h = ''; let h = '';
const status = performance.now() - GAME.statusT < 3200 ? GAME.status : ''; const status = performance.now() - GAME.statusT < 3200 ? GAME.status : '';
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(); const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld();
c.hidden = t === 'build' && !GAME.paste && !GAME.selDrag && !sel;
if (c.hidden) return;
const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length; const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length;
if (sel === 'cut') { if (sel === 'cut') {
h = `<div class="row"><span class="chip bad">✂ Cut</span><span class="name grow">Hold X and drag a box</span></div> h = `<div class="row"><span class="chip bad">✂ Cut</span><span class="name grow">Hold X and drag a box</span></div>
@@ -180,26 +182,6 @@ function renderSelCard() {
<div class="sub"><b>Left</b> sucks up ${sucking}${list.length && f >= 0 ? ' (untick them all to pick by what you click on)' : ''}. Standing still, <kbd>Shift</kbd> takes everything.</div> <div class="sub"><b>Left</b> sucks up ${sucking}${list.length && f >= 0 ? ' (untick them all to pick by what you click on)' : ''}. Standing still, <kbd>Shift</kbd> takes everything.</div>
<div class="sub"><b>Right</b> blows ${GAME.blowMix ? 'an even mix of the ticked materials' : m ? 'only ' + esc(MD[m].name.toLowerCase()) : 'the chosen material'} out. ${mixL.length > 1 || GAME.blowMix ? '<kbd>Q</kbd>/<kbd>E</kbd> switch between single materials and the ticked mix.' : 'Tick 2+ materials to blow them as a mix.'}</div> <div class="sub"><b>Right</b> blows ${GAME.blowMix ? 'an even mix of the ticked materials' : m ? 'only ' + esc(MD[m].name.toLowerCase()) : 'the chosen material'} out. ${mixL.length > 1 || GAME.blowMix ? '<kbd>Q</kbd>/<kbd>E</kbd> switch between single materials and the ticked mix.' : 'Tick 2+ materials to blow them as a mix.'}</div>
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">nozzle ${GAME.brush[1]}</span>${list.map(k => `<span class="chip"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(k)}"></span>${esc(MD[k].name)}</span>`).join('')}${list.length ? '' : '<span class="sub">Nothing ticked. <kbd>R</kbd> ticks what is under the cursor.</span>'}</div>`; <div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">nozzle ${GAME.brush[1]}</span>${list.map(k => `<span class="chip"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(k)}"></span>${esc(MD[k].name)}</span>`).join('')}${list.length ? '' : '<span class="sub">Nothing ticked. <kbd>R</kbd> ticks what is under the cursor.</span>'}</div>`;
} else if (t === 'build') {
const P = piece();
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
// while dragging a line of belts, filters or tubes, the direction is the drag's
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓' })[d] || '';
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
const cost = pieceCostNow(P);
const size = P.kind === 'machine' ? `${P.bw}×${P.bh} blocks` : P.kind === 'mold' ? `${MOLDS[P.mold].bw}×${MOLDS[P.mold].bh} blocks` : '1 block';
const fc = GAME.filterCfg;
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
const variant = pieceUsesWall(P) ? `<span class="chip hot"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R</span>`
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
: dir ? dirChip : '';
h = `<div class="row cycle"><kbd>Q</kbd><span class="name grow">${esc(P.name)}</span>${variant}<kbd>E</kbd></div>
<div class="row">${chipCost(cost)}<span class="chip">${size}</span>${P.power ? `<span class="chip">${P.power} power</span>` : ''}${GAME.ctrl ? '<span class="chip hot">Replace · Ctrl</span>' : ''}</div>
<div class="sub">${esc(P.desc)}</div>
${P.filter ? `<div class="sub">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
<div class="sub">${P.kind === 'block' ? 'Drag for a line (any length: what is out of reach becomes ghosts), Shift-drag for a rectangle.' : 'Click to place.'}${P.dirs && P.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses <kbd>R</kbd>.' : P.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(P) ? ' <kbd>R</kbd> picks the material.' : ''}</div>
<div class="sub keys-mini"><kbd>Ctrl</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>${ng ? `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</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>`;
} }
+19 -2
View File
@@ -208,6 +208,23 @@ with sync_playwright() as p:
ok = select('3', "piece().id === 'wall'") ok = select('3', "piece().id === 'wall'")
ev('GAME.wallMat = RSTONE; hudDirty = true') ev('GAME.wallMat = RSTONE; hudDirty = true')
check('build tool starts on wall blocks', ok) check('build tool starts on wall blocks', ok)
settle(150)
check('the build tool hides the top-left card', not pg.is_visible('#selcard'))
check('the build menu has no short description row', pg.locator('.bb-tip').count() == 0)
for id in ['wall', 'grate']:
pg.hover(f'#buildbar [data-piece="{id}"]'); settle(120)
check(f'hover shows the full {id} description and name', pg.is_visible('#bb-pop') and pg.inner_text('#bb-pop .bb-title') == ev(f'PIECE_BY_ID.{id}.name') and pg.inner_text('#bb-pop .bb-desc') == ev(f'PIECE_BY_ID.{id}.desc'))
pg.mouse.move(400, 250); settle(120)
check('leaving the build bar hides the popup', not pg.is_visible('#bb-pop'))
ev('GAME.usingPad = true; renderBuildBar()')
check('gamepad shows the selected piece popup', pg.is_visible('#bb-pop') and pg.inner_text('#bb-pop .bb-title') == ev('piece().name'))
ev('GAME.usingPad = false; renderBuildBar()')
for key in ['1', '2']:
pg.keyboard.press(key); settle(150)
check(f'tool {key} restores the top-left card', pg.is_visible('#selcard'))
pg.keyboard.press('3'); settle(150)
pg.mouse.click(*cell_to_screen(bx, gy - 20), button='middle'); settle(120)
check('empty middle-click pick shows its status in the build bar', 'Nothing built here to pick.' in pg.inner_text('#buildbar .bb-status'))
c0 = ev('GAME.buildCat != null ? GAME.buildCat : catOf(piece().id)') c0 = ev('GAME.buildCat != null ? GAME.buildCat : catOf(piece().id)')
pg.keyboard.press('Shift+E') pg.keyboard.press('Shift+E')
check('Shift+E switches build tabs and selects a piece in the tab', ev('GAME.buildCat') == (c0 + 1) % ev('BUILD_CATS.length') and ev('BUILD_CATS[GAME.buildCat][1].includes(piece().id)')) check('Shift+E switches build tabs and selects a piece in the tab', ev('GAME.buildCat') == (c0 + 1) % ev('BUILD_CATS.length') and ev('BUILD_CATS[GAME.buildCat][1].includes(piece().id)'))
@@ -291,7 +308,7 @@ with sync_playwright() as p:
check('belts dragged to the right run right', ev(f'(() => {{ for (let bx = {bbx} - 6; bx <= {bbx} + 2; bx++) if (blockV[{bby} * BW + bx] !== 1) return false; return true; }})()')) check('belts dragged to the right run right', ev(f'(() => {{ for (let bx = {bbx} - 6; bx <= {bbx} + 2; bx++) if (blockV[{bby} * BW + bx] !== 1) return false; return true; }})()'))
# copy: hold C and drag a box over them # copy: hold C and drag a box over them
pg.mouse.move(*blk(bbx - 6, bby)); settle(300); pg.keyboard.down('c'); settle(200) pg.mouse.move(*blk(bbx - 6, bby)); settle(300); pg.keyboard.down('c'); settle(200)
check('holding C shows copy on the card', 'Copy' in ev("$('selcard').textContent") and ev("selModeHeld()") == 'copy', ev("$('selcard').textContent")[:80]) check('holding C shows copy on the card', pg.is_visible('#selcard') and 'Copy' in ev("$('selcard').textContent") and ev("selModeHeld()") == 'copy', ev("$('selcard').textContent")[:80])
pg.keyboard.up('c'); settle(100) pg.keyboard.up('c'); settle(100)
drag(lambda: cell_to_screen((bbx - 6) * 4 + 1, bby * 4 + 1), lambda: cell_to_screen((bbx + 2) * 4 + 2, bby * 4 + 2), 5, hold_key='c') drag(lambda: cell_to_screen((bbx - 6) * 4 + 1, bby * 4 + 1), lambda: cell_to_screen((bbx + 2) * 4 + 2, bby * 4 + 2), 5, hold_key='c')
check('blueprint copied', ev('!!GAME.clip && GAME.paste && GAME.clip.blocks.length === 9'), ev('GAME.status')) check('blueprint copied', ev('!!GAME.clip && GAME.paste && GAME.clip.blocks.length === 9'), ev('GAME.status'))
@@ -324,7 +341,7 @@ with sync_playwright() as p:
pg.mouse.down(); pg.mouse.up(); settle(100) pg.mouse.down(); pg.mouse.up(); settle(100)
check('a fan on a belt says to hold Ctrl', 'Ctrl' in ev('GAME.status'), ev('GAME.status')) check('a fan on a belt says to hold Ctrl', 'Ctrl' in ev('GAME.status'), ev('GAME.status'))
pg.keyboard.down('Control'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200) pg.keyboard.down('Control'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200)
check('holding Ctrl shows replace mode', 'Replace' in ev("$('selcard').textContent")) check('holding Ctrl shows replace mode', 'Replace' in ev("$('buildbar').textContent"))
pg.screenshot(path=str(out / '07g-replace-fan.png')) pg.screenshot(path=str(out / '07g-replace-fan.png'))
pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('Control'); settle(100) pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('Control'); settle(100)
check('Ctrl-click puts the fan in place of the belt', ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4 && m.y === ({bby} + 1) * 4)") and ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0, ev('GAME.status')) check('Ctrl-click puts the fan in place of the belt', ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4 && m.y === ({bby} + 1) * 4)") and ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0, ev('GAME.status'))