diff --git a/docs/agent-log.md b/docs/agent-log.md index 6f35053..b645207 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,16 @@ 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-27: bag counted things its list didn't show (Claude Code) + +- Owner's exported save: bag total 1,522 but the list showed nothing for 1,000 basalt (from the + dev console's give), 200 clay (source not traced) and 322 molten slag (vacuumed with the Flask; + not in BAG_ORDER). `bagAdd` now stores solids as their drill yield; `deserializeWorld` converts + solids already in a saved bag; `bagMaterials` appends anything outside BAG_ORDER; dev `give` + reports what it added and refuses gases and solids with no yield. +- Files: `src/sim/items.js`, `src/sim/save.js`, `src/ui/hud.js`, `src/sim/devconsole.js`, + `test/t_basics.js`, `test/t_dev_console.js`. Tests: `python3 test/run_all.py --browser` all passed. + ## 2026-10-27: t50 resonance ping (Claude Code) - Right click with the drill: `firePing` (`src/ui/actions.js`) calls `resonancePing` (`src/sim/drill.js`), diff --git a/src/sim/devconsole.js b/src/sim/devconsole.js index ff7c609..e953c88 100644 --- a/src/sim/devconsole.js +++ b/src/sim/devconsole.js @@ -57,9 +57,10 @@ function devCommand(text, cursor) { if (!args.length || !Number.isInteger(n) || n <= 0) return usage(); const m = devMaterial(args.join(' ')); if (typeof m === 'string') return [m]; - if (m === EMPTY) return ['Air cannot go in the bag.']; + if (m === EMPTY || PH[m] === PH_GAS || (PH[m] === PH_SOLID && !YLD[m])) return [`${MD[m].name} cannot go in the bag.`]; if (n > 2147483647 - bag[m]) return ['Too many: a material count cannot exceed 2147483647.']; - bagAdd(m, n, true); result = `Added ${n} ${MD[m].name}.`; break; + const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m; + bagAdd(m, n, true); result = `Added ${n} ${MD[into].name}${into !== m ? ` (as ${MD[m].name.toLowerCase()} breaks up)` : ''}.`; break; } case 'bit': case 'bag': { const tiers = cmd === 'bit' ? BIT_TIERS : TANK_TIERS; diff --git a/src/sim/items.js b/src/sim/items.js index be9b2f1..2e27dc3 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -279,6 +279,7 @@ function entsInCircle(cx, cy, r) { // ---------- Bag ---------- function bagAdd(m, n, force) { + 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); bag[m] += k; bagTotal += k; diff --git a/src/sim/save.js b/src/sim/save.js index 0b87435..486e44e 100644 --- a/src/sim/save.js +++ b/src/sim/save.js @@ -117,6 +117,8 @@ function deserializeWorld(meta, buffers) { 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 + // 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]; Object.assign(player, meta.player); seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; }); diff --git a/src/ui/hud.js b/src/ui/hud.js index ddc83e5..5b8a143 100644 --- a/src/ui/hud.js +++ b/src/ui/hud.js @@ -26,7 +26,13 @@ function selectTool(k) { if (k === GAME.tool) return; GAME.tool = k; GAME.drag = null; hudDirty = true; renderHotbar(); playSfx('ui'); } -function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); } +// The bag's usual order first, then anything else it holds, so nothing in it is ever hidden. +const IN_BAG_ORDER = new Uint8Array(NMAT); for (const m of BAG_ORDER) IN_BAG_ORDER[m] = 1; +function bagMaterials() { + const l = BAG_ORDER.filter(m => bag[m] > 0); + for (let m = 1; m < NMAT; m++) if (bag[m] > 0 && !IN_BAG_ORDER[m]) l.push(m); + return l; +} function blowList() { return [...GAME.blow].filter(m => bag[m] > 0); } // Shift + right click: an even mix of everything loose in the bag (bars never mix) function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); } diff --git a/test/t_basics.js b/test/t_basics.js index 4a35b75..0a4c59b 100644 --- a/test/t_basics.js +++ b/test/t_basics.js @@ -148,4 +148,13 @@ const screwCatch = run(`(() => { return {airborne, caught, bag: bag[SAND]}; })()`); check('vacuum held under a screw arc catches its launched sand', screwCatch.airborne === 16 && screwCatch.caught === 16 && screwCatch.bag === 16, JSON.stringify(screwCatch)); +// Rock in the bag: solids go in as what drilling them gives, and older saves holding solids load +// that way too, so the bag never holds something its list can't show. +flatWorld(run, 120, 80, 60); +check('putting rock in the bag stores its rubble', run('(() => { bag.fill(0); bagTotal = 0; bagAdd(BASALT, 10, true); bagAdd(CLAY, 5, true); return bag[BASALT] === 0 && bag[RBAS] === 10 && bag[CLAY] === 0 && bag[LCLAY] === 5 && bagTotal === 15; })()')); +check('a save holding solid rock loads it as rubble with the same total', run(`(() => { + bag.fill(0); bagTotal = 0; const s = serializeWorld(); s.meta.bag[BASALT] = 1000; s.meta.bag[CLAY] = 200; s.meta.bag[MSLAG] = 7; + deserializeWorld(s.meta, s.buffers); + return bag[BASALT] === 0 && bag[RBAS] === 1000 && bag[CLAY] === 0 && bag[LCLAY] === 200 && bag[MSLAG] === 7 && bagTotal === 1207; +})()`)); done(); diff --git a/test/t_dev_console.js b/test/t_dev_console.js index bde7595..7d76c77 100644 --- a/test/t_dev_console.js +++ b/test/t_dev_console.js @@ -70,4 +70,6 @@ check('dev mark and command log survive save round trip', run('devWorld && devSa check('load resets speed, free building, pause and pending steps', run('devSpeed === 1 && !devFree && !devPaused && devSteps === 0')); run('alloc(240,160)'); check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0')); +check('give turns rock into its rubble and says so', run(`(() => { enableDevConsole(); const before = bag[RBAS]; const out = devCommand('give basalt 50').join(' '); return bag[RBAS] === before + 50 && bag[BASALT] === 0 && /Basalt rubble/.test(out); })()`)); +check('give refuses things that cannot be in a bag', run(`(() => { enableDevConsole(); const out = devCommand('give steam 5').join(' '); return /cannot go in the bag/.test(out) && bag[STEAM] === 0; })()`)); done();