Build range follows the screen; out-of-range replace ghosts replace (t64)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -470,7 +470,8 @@ above, including:
|
|||||||
building needs a range limit at all, since the bot needs no arms to build. Agreed: keep a limit,
|
building needs a range limit at all, since the bot needs no arms to build. Agreed: keep a limit,
|
||||||
but make it about what's on screen, so anything the player can see and aim at builds directly
|
but make it about what's on screen, so anything the player can see and aim at builds directly
|
||||||
and anything beyond stays a blueprint built when the bot comes near. No limit at all would let
|
and anything beyond stays a blueprint built when the bot comes near. No limit at all would let
|
||||||
players build in places they've never been and link far claims instantly. Not built yet.
|
players build in places they've never been and link far claims instantly. Built: the range is
|
||||||
|
the visible view box, at least 64 grains from its centre and at most 200 when zoomed far out.
|
||||||
- **Sticky materials slide freely on built surfaces (t61, 2026-10-27).** Stickiness (STICK) is the
|
- **Sticky materials slide freely on built surfaces (t61, 2026-10-27).** Stickiness (STICK) is the
|
||||||
chance per tick a grain takes a diagonal step, which made clay lumps, snow, wood chunks, ash and
|
chance per tick a grain takes a diagonal step, which made clay lumps, snow, wood chunks, ash and
|
||||||
dirt creep down sloped walls (a single clay grain never got down a 20-piece slope). The owner
|
dirt creep down sloped walls (a single clay grain never got down a 20-piece slope). The owner
|
||||||
|
|||||||
+6
-5
@@ -315,9 +315,10 @@ Bars and other loose objects (ents), and the bag: adding, taking, costs and refu
|
|||||||
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
||||||
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
||||||
|
|
||||||
### src/sim/tools.js (52 lines)
|
### src/sim/tools.js (65 lines)
|
||||||
Tool helpers: reach, brushes, and plain digging.
|
Tool helpers: reach, brushes, and plain digging.
|
||||||
- functions: inReach, forBrush, drillReach, inDrillReach, toolDig
|
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
|
||||||
|
- variables: BUILD_MAX, buildBox
|
||||||
|
|
||||||
### src/sim/drill.js (161 lines)
|
### src/sim/drill.js (161 lines)
|
||||||
The resonance drill: tuned pulses that shatter one material.
|
The resonance drill: tuned pulses that shatter one material.
|
||||||
@@ -342,7 +343,7 @@ Player actions on built things: take back, place a machine or mold, gates, F to
|
|||||||
Blueprints: copy, cut, flip, cost and paste (as ghosts) a region of built pieces.
|
Blueprints: copy, cut, flip, cost and paste (as ghosts) a region of built pieces.
|
||||||
- functions: copyRegion, flipBlueprint, bpCost, pasteGhost, cutRegion, footprintBlocker, clearFootprint
|
- functions: copyRegion, flipBlueprint, bpCost, pasteGhost, cutRegion, footprintBlocker, clearFootprint
|
||||||
|
|
||||||
### src/sim/undo.js (87 lines)
|
### src/sim/undo.js (93 lines)
|
||||||
Undo (Ctrl+Z) for building, and the bot building ghosts.
|
Undo (Ctrl+Z) for building, and the bot building ghosts.
|
||||||
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
|
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
|
||||||
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
|
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
|
||||||
@@ -511,7 +512,7 @@ Keyboard and mouse listeners.
|
|||||||
- functions: isTyping
|
- functions: isTyping
|
||||||
- variables: canvas
|
- variables: canvas
|
||||||
|
|
||||||
### src/ui/actions.js (242 lines)
|
### src/ui/actions.js (243 lines)
|
||||||
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
||||||
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
||||||
- variables: ovShown
|
- variables: ovShown
|
||||||
@@ -544,7 +545,7 @@ Shared world colours, minimap and full map.
|
|||||||
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
|
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
|
||||||
- variables: MAP_S, MAP
|
- variables: MAP_S, MAP
|
||||||
|
|
||||||
### src/ui/loop.js (136 lines)
|
### src/ui/loop.js (137 lines)
|
||||||
The frame loop and boot.
|
The frame loop and boot.
|
||||||
- functions: scanVisible, frame, boot
|
- functions: scanVisible, frame, boot
|
||||||
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ 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-27: t64 build range about the screen; out-of-range replace ghosts (Claude Code)
|
||||||
|
|
||||||
|
- `setBuildRange`/`inBuildRange` in `src/sim/tools.js`: the UI sets the view box each frame
|
||||||
|
(`src/ui/loop.js`); half-sizes clamped to REACH..BUILD_MAX (64..200 grains); with no view (tests)
|
||||||
|
a REACH box around the bot. Used by `blockInReach`, machine/mold placement and previews,
|
||||||
|
right-click take-back, hover reach, and ghost building for blocks, machines and molds.
|
||||||
|
- A replace (G) placed beyond range stores a replace flag on the ghost (`[code, v, extra, 1]`);
|
||||||
|
`ghostTick0` removes the old piece (refunded) and places the new one (charged), or re-sets a
|
||||||
|
filter's settings, when the bot comes in range.
|
||||||
|
- Files: `src/sim/tools.js`, `src/sim/blocks.js`, `src/sim/undo.js`, `src/ui/actions.js`,
|
||||||
|
`src/ui/previews.js`, `src/ui/info.js`, `src/ui/loop.js`, `test/t_build.js`, `docs/DESIGN.md`,
|
||||||
|
`docs/PROJECT_MAP.md`. Tests: `python3 test/run_all.py --browser` all passed. Not played by hand.
|
||||||
|
|
||||||
## 2026-10-27: t49 throughput overlay (Claude Code)
|
## 2026-10-27: t49 throughput overlay (Claude Code)
|
||||||
|
|
||||||
- The I overlay also shows grains per second at each line's open outlet, an orange glow on
|
- The I overlay also shows grains per second at each line's open outlet, an orange glow on
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ function blockRect(bx0, by0, bx1, by1) {
|
|||||||
for (let y = Math.min(by0, by1); y <= Math.max(by0, by1); y++) for (let x = Math.min(bx0, bx1); x <= Math.max(bx0, bx1); x++) out.push([x, y]);
|
for (let y = Math.min(by0, by1); y <= Math.max(by0, by1); y++) for (let x = Math.min(bx0, bx1); x <= Math.max(bx0, bx1); x++) out.push([x, y]);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
function blockInReach(bx, by) { return inReach(bx * BS + BS / 2, by * BS + BS / 2); }
|
function blockInReach(bx, by) { return inBuildRange(bx * BS + BS / 2, by * BS + BS / 2); }
|
||||||
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
|
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
|
||||||
function blockIs(b, code, v, extra) {
|
function blockIs(b, code, v, extra) {
|
||||||
if (blockP[b] !== code) return false;
|
if (blockP[b] !== code) return false;
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
// Tool helpers: reach, brushes, and plain digging.
|
// Tool helpers: reach, brushes, and plain digging.
|
||||||
// ---------- Tools ----------
|
// ---------- Tools ----------
|
||||||
|
// Building reaches about as far as the screen shows (t64): the view box the UI sets each frame
|
||||||
|
// (setBuildRange), at least REACH from its centre and at most BUILD_MAX; anything beyond becomes
|
||||||
|
// a blueprint ghost. With no view (tests, headless), a REACH box around the bot.
|
||||||
|
const BUILD_MAX = 200;
|
||||||
|
let buildBox = null;
|
||||||
|
function setBuildRange(cx, cy, w, h) {
|
||||||
|
const hw = Math.max(REACH, Math.min(BUILD_MAX, w / 2)), hh = Math.max(REACH, Math.min(BUILD_MAX, h / 2));
|
||||||
|
if (!buildBox) buildBox = { cx, cy, hw, hh }; else { buildBox.cx = cx; buildBox.cy = cy; buildBox.hw = hw; buildBox.hh = hh; }
|
||||||
|
}
|
||||||
|
function inBuildRange(x, y) {
|
||||||
|
if (!buildBox) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return Math.abs(dx) <= REACH && Math.abs(dy) <= REACH; }
|
||||||
|
return Math.abs(x - buildBox.cx) <= buildBox.hw && Math.abs(y - buildBox.cy) <= buildBox.hh;
|
||||||
|
}
|
||||||
function inReach(x, y) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return dx * dx + dy * dy <= REACH * REACH; }
|
function inReach(x, y) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return dx * dx + dy * dy <= REACH * REACH; }
|
||||||
function forBrush(cx, cy, r, fn) {
|
function forBrush(cx, cy, r, fn) {
|
||||||
const r2 = (r + 0.5) * (r + 0.5);
|
const r2 = (r + 0.5) * (r + 0.5);
|
||||||
|
|||||||
+9
-3
@@ -54,13 +54,19 @@ function ghostTick0() {
|
|||||||
while (budget > 0 && checks-- > 0 && ghostBlocks.size) {
|
while (budget > 0 && checks-- > 0 && ghostBlocks.size) {
|
||||||
let nx = ghostIter.next();
|
let nx = ghostIter.next();
|
||||||
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
||||||
const [b, [code, v, e]] = nx.value;
|
const [b, [code, v, e, swap]] = nx.value;
|
||||||
const bx = b % BW, by = (b / BW) | 0;
|
const bx = b % BW, by = (b / BW) | 0;
|
||||||
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
|
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
|
||||||
if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
||||||
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
||||||
}
|
}
|
||||||
if (!blockInReach(bx, by)) continue;
|
if (!blockInReach(bx, by)) continue;
|
||||||
|
// a replacing ghost takes down what is there (refunded) and builds itself, like a replace in reach
|
||||||
|
if (swap && blockP[b]) {
|
||||||
|
if (blockP[b] === code && isFilter(code)) { blockV[b] = filterVariant(code, v); setFilter(b, e); filtersDirty = true; ghostBlocks.delete(b); ghostBlocked.delete(b); budget--; continue; }
|
||||||
|
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||||
|
removeBlock(bx, by);
|
||||||
|
}
|
||||||
const err = canPlaceBlock(bx, by, code);
|
const err = canPlaceBlock(bx, by, code);
|
||||||
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
|
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
|
||||||
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||||
@@ -70,7 +76,7 @@ function ghostTick0() {
|
|||||||
}
|
}
|
||||||
for (let k = ghostMachines.length - 1; k >= 0 && budget > 0; k--) {
|
for (let k = ghostMachines.length - 1; k >= 0 && budget > 0; k--) {
|
||||||
const g = ghostMachines[k], P = PIECE_BY_ID[g.type];
|
const g = ghostMachines[k], P = PIECE_BY_ID[g.type];
|
||||||
if (!inReach(g.bx * BS + P.w / 2, g.by * BS + P.h / 2)) continue;
|
if (!inBuildRange(g.bx * BS + P.w / 2, g.by * BS + P.h / 2)) continue;
|
||||||
if (canPlaceMachine(g.type, g.bx, g.by)) continue;
|
if (canPlaceMachine(g.type, g.bx, g.by)) continue;
|
||||||
if (!canAfford(P.cost, 1)) { ghostMissing++; continue; }
|
if (!canAfford(P.cost, 1)) { ghostMissing++; continue; }
|
||||||
placeMachineAt(g.type, g.dir, g.bx, g.by); ghostMachines.splice(k, 1); budget--;
|
placeMachineAt(g.type, g.dir, g.bx, g.by); ghostMachines.splice(k, 1); budget--;
|
||||||
@@ -78,7 +84,7 @@ function ghostTick0() {
|
|||||||
}
|
}
|
||||||
for (let k = ghostMolds.length - 1; k >= 0 && budget > 0; k--) {
|
for (let k = ghostMolds.length - 1; k >= 0 && budget > 0; k--) {
|
||||||
const g = ghostMolds[k], md = MOLDS[g.kind];
|
const g = ghostMolds[k], md = MOLDS[g.kind];
|
||||||
if (!inReach(g.bx * BS + md.w / 2, g.by * BS + md.h / 2)) continue;
|
if (!inBuildRange(g.bx * BS + md.w / 2, g.by * BS + md.h / 2)) continue;
|
||||||
if (canPlaceMold(g.kind, g.bx, g.by)) continue;
|
if (canPlaceMold(g.kind, g.bx, g.by)) continue;
|
||||||
if (!canAfford(wallCost(g.wall, md.walls), 1)) { ghostMissing++; continue; }
|
if (!canAfford(wallCost(g.wall, md.walls), 1)) { ghostMissing++; continue; }
|
||||||
addMold(g.kind, g.bx, g.by, g.wall, false); ghostMolds.splice(k, 1); budget--;
|
addMold(g.kind, g.bx, g.by, g.wall, false); ghostMolds.splice(k, 1); budget--;
|
||||||
|
|||||||
+4
-3
@@ -131,7 +131,7 @@ function primaryPress(shift) {
|
|||||||
else {
|
else {
|
||||||
const { bw, bh } = pieceSize(P);
|
const { bw, bh } = pieceSize(P);
|
||||||
const { bx, by } = stampOrigin(bw, bh);
|
const { bx, by } = stampOrigin(bw, bh);
|
||||||
if (!inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
if (!inBuildRange(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
||||||
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
|
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
|
||||||
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
||||||
let err = '';
|
let err = '';
|
||||||
@@ -178,7 +178,8 @@ function primaryRelease(shift) {
|
|||||||
for (const [bx, by] of far) {
|
for (const [bx, by] of far) {
|
||||||
const b = by * BW + bx;
|
const b = by * BW + bx;
|
||||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
|
||||||
ghostBlocks.set(b, ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
|
// held G over a different piece: the ghost replaces it when the bot comes in range
|
||||||
|
ghostBlocks.set(b, GAME.replace && blockP[b] ? [code, v, ex, 1] : ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
|
||||||
}
|
}
|
||||||
if (ghosts) undoNote(g);
|
if (ghosts) undoNote(g);
|
||||||
return res;
|
return res;
|
||||||
@@ -230,7 +231,7 @@ function toolTick() {
|
|||||||
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
||||||
if (!GAME.rmbSeen.has(key) && inReach(ax, ay)) {
|
if (!GAME.rmbSeen.has(key) && inBuildRange(ax, ay)) {
|
||||||
GAME.rmbSeen.add(key);
|
GAME.rmbSeen.add(key);
|
||||||
const keep = undoRec; undoRec = GAME.rmbRec;
|
const keep = undoRec; undoRec = GAME.rmbRec;
|
||||||
const msg = removePieceAt(ax, ay);
|
const msg = removePieceAt(ax, ay);
|
||||||
|
|||||||
+1
-1
@@ -158,7 +158,7 @@ function hoverInfo(ax, ay) {
|
|||||||
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint);
|
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint);
|
||||||
if (player.lens) t2.push(fmtT(temp[i]));
|
if (player.lens) t2.push(fmtT(temp[i]));
|
||||||
}
|
}
|
||||||
const reachOk = toolId() === 'drill' || inReach(ax, ay);
|
const reachOk = toolId() === 'drill' || (toolId() === 'build' ? inBuildRange(ax, ay) : inReach(ax, ay));
|
||||||
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
|
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
|
||||||
// what it does, unless the cursor is on a machine or mold, which have their own details
|
// what it does, unless the cursor is on a machine or mold, which have their own details
|
||||||
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
|
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function frame(now) {
|
|||||||
// grid under the cursor does not slide around mid-line.
|
// grid under the cursor does not slide around mid-line.
|
||||||
if (GAME.drag) { lookX = GAME.lookHold.x; lookY = GAME.lookHold.y; } else { GAME.lookHold.x = lookX; GAME.lookHold.y = lookY; }
|
if (GAME.drag) { lookX = GAME.lookHold.x; lookY = GAME.lookHold.y; } else { GAME.lookHold.x = lookX; GAME.lookHold.y = lookY; }
|
||||||
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
||||||
|
{ const v = viewCells(); setBuildRange(R.camX + v.w / 2, R.camY + v.h / 2, v.w, v.h); }
|
||||||
R.frame++;
|
R.frame++;
|
||||||
drawWorld();
|
drawWorld();
|
||||||
drawCracks();
|
drawCracks();
|
||||||
|
|||||||
+2
-2
@@ -67,7 +67,7 @@ function drawPreviews() {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
const reachOk = inReach(ax, ay);
|
const reachOk = inReach(ax, ay);
|
||||||
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(), pasting = !sel && GAME.paste && GAME.clip;
|
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(), pasting = !sel && GAME.paste && GAME.clip;
|
||||||
if (!reachOk && t !== 'drill' && !sel) {
|
if (!reachOk && t !== 'drill' && t !== 'build' && !sel) {
|
||||||
drawReachCircle(REACH, 0.35);
|
drawReachCircle(REACH, 0.35);
|
||||||
}
|
}
|
||||||
drawDrillPulses();
|
drawDrillPulses();
|
||||||
@@ -143,7 +143,7 @@ function drawPreviews() {
|
|||||||
const { bx, by } = stampOrigin(bw, bh);
|
const { bx, by } = stampOrigin(bw, bh);
|
||||||
const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
||||||
const swap = !!err && GAME.replace && /already/.test(err) && !footprintBlocker(bx, by, bw, bh);
|
const swap = !!err && GAME.replace && /already/.test(err) && !footprintBlocker(bx, by, bw, bh);
|
||||||
const ok = (!err || swap) && inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2) && canAfford(pieceCostNow(P), 1);
|
const ok = (!err || swap) && inBuildRange(bx * BS + bw * BS / 2, by * BS + bh * BS / 2) && canAfford(pieceCostNow(P), 1);
|
||||||
const [x, y, w, h] = blockRectPx(bx, by, bw, bh);
|
const [x, y, w, h] = blockRectPx(bx, by, bw, bh);
|
||||||
if (swap && ok) { ctx.fillStyle = 'rgba(255,190,90,0.35)'; ctx.fillRect(x, y, w, h); }
|
if (swap && ok) { ctx.fillStyle = 'rgba(255,190,90,0.35)'; ctx.fillRect(x, y, w, h); }
|
||||||
if (P.kind === 'mold') {
|
if (P.kind === 'mold') {
|
||||||
|
|||||||
@@ -79,4 +79,29 @@ run('for (let y = 120; y < 122; y++) for (let x = 60; x < 62; x++) setCell(y * W
|
|||||||
const n = run('clearHardSpecks(true)');
|
const n = run('clearHardSpecks(true)');
|
||||||
check('a 4-grain speck of iron ore in stone becomes stone; a 100-grain vein stays', n === 1 && count(run, run('FEV')) === 100 && run('mat[120 * W + 60]') === run('STONE'), `${n} specks, ${count(run, run('FEV'))} vein`);
|
check('a 4-grain speck of iron ore in stone becomes stone; a 100-grain vein stays', n === 1 && count(run, run('FEV')) === 100 && run('mat[120 * W + 60]') === run('STONE'), `${n} specks, ${count(run, run('FEV'))} vein`);
|
||||||
|
|
||||||
|
// t64: building reaches about as far as the screen shows, and a replace placed out of range
|
||||||
|
// becomes a ghost that replaces the old piece when the bot comes in range.
|
||||||
|
fresh();
|
||||||
|
check('with no view the build range is a REACH box around the bot', run('buildBox = null; inBuildRange(bot.x + bot.w / 2 + REACH, bot.y) && !inBuildRange(bot.x + bot.w / 2 + REACH + 2, bot.y)'));
|
||||||
|
check('the build range follows the view box', run('(() => { setBuildRange(200, 100, 300, 160); const r = inBuildRange(340, 100) && !inBuildRange(352, 100) && inBuildRange(200, 180) && !inBuildRange(200, 190); buildBox = null; return r; })()'));
|
||||||
|
check('a tiny view still reaches REACH and a huge one stops at BUILD_MAX', run('(() => { setBuildRange(500, 500, 20, 20000); const r = buildBox.hw === REACH && buildBox.hh === BUILD_MAX; buildBox = null; return r; })()'));
|
||||||
|
fresh();
|
||||||
|
const swap = run(`(() => {
|
||||||
|
placeBlock(BP_BELT, 1, 50, 20, true); const b = 20 * BW + 50, m0 = money;
|
||||||
|
ghostBlocks.set(b, [BP_SCREW, 1, undefined, 1]);
|
||||||
|
bot.x = 20; bot.y = 20; for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
const before = blockP[b];
|
||||||
|
bot.x = 50 * BS - 4; bot.y = 20 * BS - 10; for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
return { before, after: blockP[b], v: blockV[b], spent: m0 - money, ghosts: ghostBlocks.size };
|
||||||
|
})()`);
|
||||||
|
check('an out-of-range replace ghost leaves the old piece until the bot comes near', swap.before === run('BP_BELT'), JSON.stringify(swap));
|
||||||
|
check('then replaces it: the new piece is built and the ghost is gone', swap.after === run('BP_SCREW') && swap.v === 1 && swap.ghosts === 0, JSON.stringify(swap));
|
||||||
|
check('the replace charges the new piece and refunds the old one', swap.spent === 25 - 12, JSON.stringify(swap));
|
||||||
|
fresh();
|
||||||
|
check('a plain ghost over a different piece still leaves it alone', run(`(() => {
|
||||||
|
placeBlock(BP_BELT, 1, 30, 20, true); const b = 20 * BW + 30;
|
||||||
|
ghostBlocks.set(b, [BP_SCREW, 1]); bot.x = 30 * BS - 4; bot.y = 20 * BS - 10;
|
||||||
|
for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
return blockP[b] === BP_BELT && ghostBlocks.has(b);
|
||||||
|
})()`));
|
||||||
done();
|
done();
|
||||||
|
|||||||
Reference in New Issue
Block a user