Dev console refuses buildable piece materials; loose piece grains cleared on load (t97)

This commit is contained in:
2026-10-29 18:08:38 -04:00
parent 0483fb8da4
commit 3b427aeecc
9 changed files with 119 additions and 9 deletions
+6
View File
@@ -604,3 +604,9 @@ above, including:
the old feel. Restore the original advancing-front nudge: 30% times ray energy per
half-grain sample, straight back toward the bot, then down or up if blocked. Scale that
chance with sampling distance; do not keep shaking the travelled path.
- **Build pieces never go in the bag (t97, 2026-10-29).** The owner wants pieces such as saws
built from the menu: giving them as grains let the nozzle leave permanent, uncollectable
cells. Exclude dedicated construction materials from give, discovery and blowing. On load,
clear loose construction grains and their bag/nozzle inventory; preserve owned blocks,
slope seals and machines. Ordinary wall ingredients still become rubble, scrap or bars.
+4 -4
View File
@@ -317,7 +317,7 @@ Molds: cavities for castings; ingot troughs empty themselves below. Removal retu
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord
### src/sim/items.js (318 lines)
### src/sim/items.js (319 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
- 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
@@ -337,7 +337,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
- variables: treeBases, saplings, treeDraw
### src/sim/vacuum.js (199 lines)
### src/sim/vacuum.js (200 lines)
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
- variables: blowRR, FLY_G, FLY_Q
@@ -387,12 +387,12 @@ World generation helpers: noise and removal of tiny hard rock specks.
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
- functions: generateWorld, generateWorldAsync
### src/sim/devconsole.js (185 lines)
### src/sim/devconsole.js (193 lines)
Dev commands are pure simulation operations; the browser owns the panel and confirmation.
- functions: resetDevConsole, enableDevConsole, devError, devName, devGiveable, devMaterialList, devDistance, devSuggestions, devHistory, devRemember, devRecall, devComplete, devMaterial, devCommand
- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_COMMANDS, DEV_MATERIALS
### src/sim/save.js (185 lines)
### src/sim/save.js (216 lines)
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
- functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
- variables: SAVE_VERSION, TOFF
+12
View File
@@ -4,6 +4,18 @@ 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
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-29: t97 keep construction grains out of the bag (Codex)
- PARTMAT now blocks give/discovery, inventory/refunds and single/mixed blowing. Load clears
unowned piece grains and contaminated bag/nozzle entries, preserving construction and seals.
- Files: src/sim/{devconsole,items,vacuum,save}.js, test/t_dev_console.{js,py},
docs/{DESIGN,PROJECT_MAP}.md, tasks/t97_give_no_buildables.md and this log.
- Tests first: 9 new regressions failed; all 37 headless suites passed (14 new checks;
/tmp/t97-headless-final.log). Only browser suite: test/t_dev_console.py, 31 checks passed
(/tmp/t97-browser-final.log); inspected shots/t97-console.png (pixel). Sandboxed real/soft
startup timed out (soft reported WebGPU unavailable); real GPU outside sandbox passed after isolating console output in
the existing colour test. Worldgen/save format unchanged; owner review pending. No commit.
## 2026-10-29: t80 validate imported world arrays (Codex)
- Validate positive integer dimensions up to the Huge option (6144 x 2048), cell/block/fog
+3 -1
View File
@@ -24,7 +24,7 @@ const DEV_COMMANDS = {
};
function devError(text) { return Object.assign([text], { error: true }); }
function devName(text) { return text.toLowerCase().replace(/\s+/g, ''); }
function devGiveable(id) { return id !== EMPTY && PH[id] !== PH_GAS && (PH[id] !== PH_SOLID || !!YLD[id]); }
function devGiveable(id) { return id !== EMPTY && !PARTMAT[id] && PH[id] !== PH_GAS && (PH[id] !== PH_SOLID || !!YLD[id]); }
function devMaterialList(text = '') {
const keys = new Map(Object.entries(DEV_MATERIALS).map(([key, id]) => [id, key]));
const list = [], name = devName(text);
@@ -91,6 +91,7 @@ function devMaterial(name) {
name = devName(name);
const internal = Object.entries(DEV_MATERIALS).find(([key]) => devName(key) === name);
if (internal) return internal[1];
for (let id = 1; id < NMAT; id++) if (PARTMAT[id] && devName(MD[id].name) === name) return id;
const ids = devMaterialList();
const exact = ids.filter(([, id]) => devName(MD[id].name) === name);
const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => devName(key).startsWith(name) || devName(MD[id].name).startsWith(name))).map(([, id]) => id))];
@@ -134,6 +135,7 @@ function devCommand(text, cursor) {
if (!args.length || !Number.isInteger(n) || n <= 0) return usage();
const m = devMaterial(args.join(' '));
if (typeof m === 'string') return devError(m);
if (PARTMAT[m]) return devError(`${MD[m].name} is built from the build menu, not given.`);
if (!devGiveable(m)) return devError(`${MD[m].name} cannot go in the bag.`);
if (n > 2147483647 - bag[m]) return devError('Too many: a material count cannot exceed 2147483647.');
const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m;
+2 -1
View File
@@ -279,6 +279,7 @@ function entsInCircle(cx, cy, r) {
// ---------- Bag ----------
function bagAdd(m, n, force) {
if (PARTMAT[m]) return 0;
if (PH[m] === PH_SOLID && YLD[m]) m = YLD[m]; // rock and metal go in as what drilling them gives
const room = force ? n : Math.max(0, bagCap() - bagTotal);
const k = Math.min(n, room);
@@ -291,7 +292,7 @@ function bagTake(m, n) { const k = Math.min(n, bag[m]); bag[m] -= k; bagTotal -=
// grains as loose material, bars as bar objects. Counts what dropped.
let droppedLast = 0;
function giveOrDrop(m, n, x, y) {
if (n <= 0) return;
if (n <= 0 || PARTMAT[m]) return;
if (m === CASH) { money += n; return; }
const k = bagAdd(m, n, false);
let rest = n - k;
+9 -2
View File
@@ -139,8 +139,8 @@ function deserializeWorld(meta, buffers) {
weather.shower = mw.shower || null; weather.next = mw.next || weather.next; weather.wind = mw.wind || weather.wind; weather.seen = !!mw.seen; weather.base = mw.base || 0;
}
bot.x = meta.bot.x; bot.y = meta.bot.y; bot.lamp = meta.bot.lamp !== false; bot.vx = bot.vy = 0;
bag = new Int32Array(NMAT); meta.bag.forEach((v, k) => { if (k < NMAT) bag[k] = v; });
for (const m of meta.flyers || []) if (m < NMAT) bag[m]++; // grains that were mid-air when saved
bag = new Int32Array(NMAT); meta.bag.forEach((v, k) => { if (k < NMAT && !PARTMAT[k]) bag[k] = v; });
for (const m of meta.flyers || []) if (m < NMAT && !PARTMAT[m]) bag[m]++; // grains that were mid-air when saved
// solids saved in the bag (older saves) come back as what drilling them gives
for (let k = 0; k < NMAT; k++) if (bag[k] && PH[k] === PH_SOLID && YLD[k]) { bag[YLD[k]] += bag[k]; bag[k] = 0; }
bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];
@@ -148,6 +148,13 @@ function deserializeWorld(meta, buffers) {
seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; });
ledger = meta.ledger || {};
Object.assign(stats, meta.stats || {});
// Dedicated piece materials can only survive as owned construction, never nozzle grains.
for (let i = 0; i < N; i++) if (PARTMAT[mat[i]] && !mach[i]) {
const b = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2);
if (!blockOwnsCell(b, i, false) && slopeSealOwner(i) < 0) {
setCell(i, EMPTY, temp[i], 0); markPow(i, false);
}
}
clearStraySeals();
powerDirty = true; rebuildPower(); slopeCleatsTick(false); updateRoof();
if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount
+2 -1
View File
@@ -65,12 +65,13 @@ let blowRR = 0;
function toolBlow(cx, cy, r, m, mix) {
let pickM = null;
if (mix && mix.length) {
const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask));
const avail = mix.filter(k => !PARTMAT[k] && bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask));
if (!avail.length) return { placed: 0, status: 'None of the selected materials are in the bag.' };
pickM = () => { for (let k = 0; k < avail.length; k++) { const c = avail[(blowRR++) % avail.length]; if (bag[c] > 0) return c; } return 0; };
m = avail[0];
}
if (!m) return { placed: 0, status: 'Nothing selected: click or Q/E in the bag.' };
if (PARTMAT[m]) return { placed: 0, status: MD[m].name + ' is built from the build menu.' };
if (bag[m] <= 0) return { placed: 0, status: 'None of the selected materials are in the bag.' };
const ph = PH[m];
if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' };
+68
View File
@@ -107,4 +107,72 @@ check('completion supports keywords and help commands', run("devComplete('free o
check('completion leaves unmatched input intact', run("devComplete('give zzzzz').text === 'give zzzzz'"));
check('history validates stored data', run("devHistory(null).items.length === 0 && devHistory(['ok', 5, '', ' ']).items.join() === 'ok'"));
check('history caps at 50 and navigates to empty after newest', run("(() => { const h = devHistory([]); for (let i=0; i<55; i++) devRemember(h, 'money ' + i); devRemember(h, ' '); const newest = devRecall(h, -1); for (let i=0;i<60;i++) devRecall(h,-1); const oldest = devRecall(h,-1); for(let i=0;i<60;i++) devRecall(h,1); return h.items.length === 50 && newest === 'money 54' && oldest === 'money 5' && devRecall(h,1) === ''; })()"));
// Construction materials are pieces, never loose inventory or nozzle grains.
flatWorld(run, 240, 160, 110);
run('enableDevConsole();');
check('construction material markers cover every dedicated piece material', run(`
[BELTL, BELTR, SHAFT, GATE, HULL, PIPE, DOCKW, GRINDER, SAWB, PRESSB, RACKB].every(m => PARTMAT[m])
`));
check('give refuses internal and display names of every construction material', run(`(() => {
const before = JSON.stringify([bagTotal, Array.from(bag), devLog]);
for (const [key, m] of Object.entries(DEV_MATERIALS)) if (PARTMAT[m]) {
for (const name of [key, MD[m].name]) {
const out = devCommand('give ' + name + ' 3');
if (!out.error || !out[0].includes('built from the build menu')) return false;
}
}
return before === JSON.stringify([bagTotal, Array.from(bag), devLog]);
})()`));
check('give wall refuses without changing inventory', command('give wall 3').error === true && run('bagTotal === 0'));
check('construction pieces stay out of lists, suggestions and completion', run(`
devMaterialList().every(([, m]) => !PARTMAT[m]) &&
devComplete('give saw').matches.length === 0 &&
!devSuggestions('sawb').includes(MD[SAWB].name)
`));
command('give sand 7'); command('give copper bar 2'); command('give stone 4');
check('grains, bars and wall ingredients still work', run('bag[SAND] === 7 && bag[BARCU] === 2 && bag[RSTONE] === 4'));
check('bag pickup and refund cannot introduce piece grains', run(`(() => {
const before = bagTotal;
const added = bagAdd(SAWB, 2, true);
giveOrDrop(GATE, 3, 40, 40);
return added === 0 && bagTotal === before && !bag[SAWB] && !bag[GATE] && mat[40 * W + 40] === EMPTY;
})()`));
run('bag[SAWB] = 4; bagTotal += 4;');
check('nozzle refuses even a directly contaminated bag', run('toolBlow(50, 40, 1, SAWB).placed === 0 && flyers.length === 0'));
check('mixed nozzle skips piece grains while blowing sand', run(`(() => {
const out = toolBlow(50, 40, 1, SAWB, [SAWB, SAND]);
return out.placed > 0 && flyers.every(f => f.m === SAND) && bag[SAWB] === 4;
})()`));
// Keep legitimate blocks, slopes and their outside seals, parts, machines and natural rock.
flatWorld(run, 240, 160, 110);
run(`
const builtCodes = [BP_WALL, BP_SAW, BP_GRIND, BP_PRESS, BP_RACK, BP_BELT, BP_SHAFT, BP_GATE, BP_PIPE, BP_PUMP, BP_OUTLET, BP_SCREW, BP_GRATE, BP_FILTER, BP_BELT_UP];
for (let k = 0; k < builtCodes.length; k++) placeBlock(builtCodes[k], k === 0 ? RSTONE : 0, 2 + k * 3, 10, true);
placeMachineAt('fan', 0, 4, 17, true);
addMold('ingot', 12, 17, LCLAY, true);
placeDock(180);
const ownedBefore = JSON.stringify([Array.from(mat.slice(40 * W, 80 * W)), Array.from(part.slice(40 * W, 80 * W)), Array.from(blockP)]);
const pieceIds = Object.values(DEV_MATERIALS).filter(m => PARTMAT[m]);
for (let k = 0; k < pieceIds.length; k++) setCell(20 * W + 20 + k, pieceIds[k], 20, 0);
// A nozzle grain in the open side of a real sloped belt is still loose.
const looseCorner = 40 * W + 176;
setCell(looseCorner, SAWB, 20, 0);
bag[SAWB] = 5; bag[HULL] = 2; bag[SAND] = 7; bagTotal = 14;
const straySave = serializeWorld(); straySave.meta.flyers.push(GATE, SAND);
deserializeWorld(straySave.meta, straySave.buffers);
`);
check('loading clears all loose construction grains', run('pieceIds.every((m, k) => mat[20 * W + 20 + k] === EMPTY) && mat[looseCorner] === EMPTY'));
check('loading preserves built cells, slope seals, parts, machines and molds', run(`
ownedBefore === JSON.stringify([Array.from(mat.slice(40 * W, 80 * W)), Array.from(part.slice(40 * W, 80 * W)), Array.from(blockP)]) && machines.some(m => m && m.type === 'fan') && molds.length === 1
`));
check('loading clears construction inventory and saved nozzle grains with correct totals', run('bag[SAWB] === 0 && bag[HULL] === 0 && bag[GATE] === 0 && bag[SAND] === 8 && bagTotal === 8'));
check('taking down a saw and a wall refunds ingredients without piece grains', run(`(() => {
removeBlock(5, 10); removeBlock(2, 10);
return bag[RSTONE] === 16 && Object.values(DEV_MATERIALS).filter(m => PARTMAT[m]).every(m => !bag[m]);
})()`));
check('ordinary natural rock survives cleanup', run('mat[120 * W + 20] === STONE'));
check('free building and unlock still work with construction materials excluded', run(`(() => {
enableDevConsole(); devCommand('unlock'); devCommand('free on');
return seen[SAWB] && placeBlock(BP_SAW, 0, 5, 5, false) === '' && mat[20 * W + 20] === SAWB;
})()`));
done();
+13
View File
@@ -73,6 +73,19 @@ with launch() as (browser, page, url):
check('Ctrl+L clears output and keeps console focus', page.evaluate("$('dev-output').children.length === 0 && document.activeElement === $('dev-input')"))
page.wait_for_timeout(100)
check('Tab, arrows and Ctrl+L leave bot and game controls alone', page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") == position and page.evaluate('keys.size === 0 && !GAME.ctrl'))
command('give saw 3')
check('giving a saw explains building and leaves it out of the bag',
'Saw is built from the build menu, not given.' in page.inner_text('#dev-output')
and page.evaluate('bag[SAWB] === 0'))
page.fill('#dev-input', 'give saw'); page.keyboard.press('Tab')
check('Tab never offers the saw', page.input_value('#dev-input') == 'give saw')
command('materials saw')
check('material discovery omits construction pieces', 'No matching materials.' in page.inner_text('#dev-output'))
command('give sand 7'); command('give copper bar 2'); command('give gate 3')
check('ordinary grains and bars still arrive while gates are refused',
page.evaluate('bag[SAND] === 7 && bag[BARCU] === 2 && bag[GATE] === 0'))
page.screenshot(path=str(SHOTS / 't97-console.png'))
command('clear')
command('materials copperore')
command('help give')
command('give coperore')