Dev console refuses buildable piece materials; loose piece grains cleared on load (t97)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.' };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user