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:
+3
-1
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'))
|
||||||
|
|||||||
Reference in New Issue
Block a user