';
for (const id of ids) {
@@ -59,10 +60,33 @@ function renderBuildBar() {
const sw = Q.variants === 'wall' ? `` : '';
h += ``;
}
- // just the first sentence here; the card at the top left has the rest for the chosen piece
- const first = (tip.desc.match(/^.*?[.!?](\s|$)/) || [tip.desc])[0].trim();
- h += `
${esc(tip.name)}. ${esc(first)}
`;
- bar.innerHTML = h;
+ h += '';
+ if (!bar.querySelector('.bb-menu')) bar.innerHTML = '';
+ const menu = bar.querySelector('.bb-menu');
+ 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 ? `${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag` : `${glyph(dir)} ${esc(dir)} · R`;
+ const fc = GAME.filterCfg;
+ const sws = ms => ms.slice(0, 6).map(m => ``).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
+ const variant = pieceUsesWall(P) ? `${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R`
+ : P.filter ? `${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F${dirChip}`
+ : dir ? dirChip : '';
+ bar.querySelector('.bb-status').innerHTML = variant + (GAME.ctrl ? 'Replace · Ctrl' : '')
+ + (ng ? `${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.` : '')
+ + (status ? `${esc(status)}` : '');
+ 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 = `
How to place ${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 R.' : tip.dirs ? ' R turns it.' : pieceUsesWall(tip) ? ' R picks the material.' : ''}
+ ${tip.filter ? `
New ones will ${esc(filterSentence(GAME.filterCfg))}. F sets materials and mode (or click materials in the bag, or T over one); F on a placed filter changes its whole row.
` : ''}
+
Ctrl replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
`;
+ 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() {
const bar = $('buildbar'); if (!bar) return;
diff --git a/src/ui/notebook.js b/src/ui/notebook.js
index e86ce72..eaf4df1 100644
--- a/src/ui/notebook.js
+++ b/src/ui/notebook.js
@@ -38,7 +38,7 @@ function renderNotebook() {
}
body.innerHTML = `
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.` : ''}
Material
State
Hard
What it does
Notes
${rows}
`;
} else if (nbTab === 'Pieces') {
- let h = '
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.
';
+ let h = '
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.
';
for (const P of PIECES) {
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);
diff --git a/src/ui/selection.js b/src/ui/selection.js
index 8100185..11c85e3 100644
--- a/src/ui/selection.js
+++ b/src/ui/selection.js
@@ -150,6 +150,8 @@ function renderSelCard() {
let h = '';
const status = performance.now() - GAME.statusT < 3200 ? GAME.status : '';
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;
if (sel === 'cut') {
h = `
✂ CutHold X and drag a box
@@ -180,26 +182,6 @@ function renderSelCard() {
Left sucks up ${sucking}${list.length && f >= 0 ? ' (untick them all to pick by what you click on)' : ''}. Standing still, Shift takes everything.
Right 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 ? 'Q/E switch between single materials and the ticked mix.' : 'Tick 2+ materials to blow them as a mix.'}
nozzle ${GAME.brush[1]}${list.map(k => `${esc(MD[k].name)}`).join('')}${list.length ? '' : 'Nothing ticked. R ticks what is under the cursor.'}
`;
- } 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 ? `${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag` : `${glyph(dir)} ${esc(dir)} · R`;
- 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 => ``).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
- const variant = pieceUsesWall(P) ? `${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R`
- : P.filter ? `${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F${dirChip}`
- : dir ? dirChip : '';
- h = `
New ones will ${esc(filterSentence(GAME.filterCfg))}. F sets materials and mode (or click materials in the bag, or T over one); F on a placed filter changes its whole row.
` : ''}
-
${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 R.' : P.dirs ? ' R turns it.' : pieceUsesWall(P) ? ' R picks the material.' : ''}
-
Ctrl replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
${ng ? `
${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.
` : ''}`;
} else if (t === 'ignite') {
h = `
Igniterup to 640°C
Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.
`;
}
diff --git a/test/play.py b/test/play.py
index 28b3231..87f9c0b 100644
--- a/test/play.py
+++ b/test/play.py
@@ -208,6 +208,23 @@ with sync_playwright() as p:
ok = select('3', "piece().id === 'wall'")
ev('GAME.wallMat = RSTONE; hudDirty = true')
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)')
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)'))
@@ -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; }})()'))
# copy: hold C and drag a box over them
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)
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'))
@@ -324,7 +341,7 @@ with sync_playwright() as p:
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'))
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.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'))