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 caa2463fda
commit fec4f3bfe1
8 changed files with 96 additions and 39 deletions
+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'))