Vacuum menu on F replaces the pick-up modes (t16)

Pick-up takes everything unless something is marked; F with the Vacuum
(when nothing in reach can be used) or the card chips open a menu with
pick-up and blow toggles per material. The bag rows lose their buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 02:28:56 -04:00
co-authored by Claude Opus 5.5
parent 255824ec6d
commit aa46406c92
16 changed files with 148 additions and 125 deletions
+50 -51
View File
@@ -21,14 +21,10 @@ with sync_playwright() as p:
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
check('empty marks default to first mode', pg.evaluate("GAME.pickMode === 'first'"))
pg.evaluate("toggleCollect(CUORE); setPickMode('all')")
pg.evaluate("toggleCollect(CUORE)")
pg.reload(); pg.wait_for_timeout(300)
check('pick-up marks and a manual mode persist across reload', pg.evaluate("GAME.collect.has(CUORE) && GAME.pickMode === 'all'"))
pg.evaluate("localStorage.removeItem('deepgrain.pickMode')")
pg.reload(); pg.wait_for_timeout(300)
check('old pick-up marks default to marked mode', pg.evaluate("GAME.collect.has(CUORE) && GAME.pickMode === 'marked'"))
pg.evaluate("localStorage.removeItem('deepgrain.collect'); localStorage.removeItem('deepgrain.pickMode')")
check('pick-up marks persist across reload', pg.evaluate("GAME.collect.has(CUORE)"))
pg.evaluate("localStorage.removeItem('deepgrain.collect')")
pg.reload(); pg.wait_for_timeout(300)
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
@@ -224,7 +220,7 @@ with sync_playwright() as p:
pg.screenshot(path=str(out / '01-dug.png'))
check('trees live in the background layer', ev('bgm.some(v => v === WOOD)') and ev('(() => { for (let i = 0; i < N; i++) if (mat[i] === LEAVES) return false; return true; })()'))
# 2. vacuum, left button: only the material you start on
# 2. vacuum, left button: everything unless marked
pg.keyboard.press('2'); settle(100)
tgt = ev(f'''(() => {{ for (let r = 0; r < 30; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
const x = {bx + 6} + dx, y = {gy + 3} + dy, m = mat[y*W+x]; if (PH[m] === PH_POWDER && inReach(x, y)) return [x, y, m]; }} return null; }})()''')
@@ -236,16 +232,14 @@ with sync_playwright() as p:
got = ev(f'bag[{tgt[2]}]')
others = ev(f'bagTotal - bag[{tgt[2]}]')
check('vacuum (left) picks the clicked material', got > 0, f"{ev(f'MD[{tgt[2]}].name')} x{got}")
check('vacuum (left) leaves other materials', others == 0, others)
check('vacuum defaults to What I click', ev("GAME.pickMode === 'first'"))
check('first mode locks onto dirt and leaves other grains', ev("""(() => {
check('unmarked vacuum takes both materials in a mixed pile', ev("""(() => {
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, cells = [], dirt = bag[LDIRT], stone = bag[RSTONE];
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { const i = (y+dy)*W+x+dx; cells.push([i, mat[i], temp[i], flags[i]]); setCell(i, dx < 0 ? RSTONE : LDIRT, 20, 0); }
const aim = GAME.aim; GAME.aim = { x, y }; GAME.lmb = true; primaryPress(false); toolTick();
const ok = GAME.vacFilter === LDIRT && bag[LDIRT] > dirt && bag[RSTONE] === stone;
const ok = GAME.vacFilter === -1 && bag[LDIRT] > dirt && bag[RSTONE] > stone;
primaryRelease(false); GAME.lmb = false; GAME.aim = aim;
for (const [i, m, t, f] of cells) setCell(i, m, t, f);
bagTake(LDIRT, bag[LDIRT] - dirt); return ok;
bagTake(LDIRT, bag[LDIRT] - dirt); bagTake(RSTONE, bag[RSTONE] - stone); return ok;
})()"""))
check('middle click chooses a loose material even with none in the bag', ev("""(() => {
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, i = y*W+x;
@@ -262,11 +256,10 @@ with sync_playwright() as p:
if tgt2:
cx, cy = cell_to_screen(tgt2[0], tgt2[1])
pg.mouse.move(cx, cy); settle(80); pg.keyboard.press('r'); settle(80)
check('R marks pick-up and switches mode without changing blow', ev(f"GAME.collect.has({tgt2[2]}) && GAME.pickMode === 'marked'") and ev('[...GAME.blow]') == blow_before)
check('R marks pick-up without changing blow', ev(f"GAME.collect.has({tgt2[2]})") and ev('[...GAME.blow]') == blow_before)
ev('renderBag()'); settle(50)
marks = pg.locator('#baglist [data-pick][aria-pressed=true]')
if marks.count(): marks.first.click(); settle(80)
check('removing the last pick-up mark returns to first', ev(f"!GAME.collect.has({tgt2[2]}) && GAME.pickMode === 'first'"))
pg.keyboard.press('r'); settle(80)
check('R again unmarks', ev(f"!GAME.collect.has({tgt2[2]})"))
pg.screenshot(path=str(out / '02-vacuum.png'))
# 2b. the vacuum filter is fixed when you click: ore with stone around it, ore marked
@@ -290,43 +283,28 @@ with sync_playwright() as p:
mixed_pile(px0, py0); settle(400)
ore = find_ore()
refill('bag.fill(0)')
ev('GAME.collect = new Set([CUORE]); setPickMode("marked"); lastBagKey = "";')
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
suck_at(*ore)
check('with ore marked, clicking ore never pulls in stone', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
st = ev(f'(() => {{ for (let y = {py0} - 8; y < {gy} + 4; y++) for (let x = {px0} - 6; x < {px0} + 20; x++) if (mat[y*W+x] === RSTONE && mat[(y-1)*W+x] === EMPTY && inReach(x, y)) return [x, y]; return null; }})()')
suck_at(*st)
check('with ore marked, starting on stone rubble still takes only the ore', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
ev('GAME.collect = new Set(); setPickMode("first"); lastBagKey = "";')
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
ore = find_ore()
suck_at(*ore, ms=500, shift=True, fly='d')
check('Shift held while flying boosts instead of taking everything', ev('bag[RSTONE]') == 0 or ev('bag[CUORE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
check('Shift held while flying boosts instead of taking everything', ev('bag[RSTONE]') == 0 and ev('bag[CUORE]') > 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
ev(f'bot.x = {bx} - 2; bot.y = {by} - 2; bot.vx = bot.vy = 0;'); settle(300)
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
ore = find_ore()
suck_at(*ore, ms=500, shift=True)
check('Shift held standing still takes everything', ev('bag[RSTONE]') > 0 and ev('bag[CUORE]') > 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
pg.keyboard.press('t'); settle(80)
check('T cycles first to marked', ev("GAME.pickMode === 'marked'"))
before = ev('bagTotal')
suck_at(*ore, ms=100)
check('empty marked mode picks up nothing and explains why', ev('bagTotal') == before and 'Nothing marked to pick up. R over something, or ⬇ in the bag.' in ev('GAME.status'))
pg.keyboard.press('t'); settle(80)
check('T cycles marked to all', ev("GAME.pickMode === 'all'"))
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
ore = find_ore(); suck_at(*ore, ms=500)
check('Everything mode picks up both materials', ev('bag[RSTONE] > 0 && bag[CUORE] > 0'))
pg.keyboard.press('t'); settle(80)
check('T cycles all to first', ev("GAME.pickMode === 'first'"))
for mode in ['marked', 'all', 'first']:
pg.click('#selcard [data-pick-mode]'); settle(80)
check(f'card chip cycles to {mode}', ev(f'GAME.pickMode === "{mode}"'))
ev("toggleCollect(CUORE); setPickMode('all'); toggleCollect(RSTONE)")
check('adding another mark switches a manual mode back to marked', ev("GAME.pickMode === 'marked'"))
ev("setPickMode('all'); toggleCollect(RSTONE)")
check('removing a mark keeps a manual mode', ev("GAME.pickMode === 'all'"))
ev("GAME.collect.clear(); setPickMode('first'); lastBagKey = ''; hudDirty = true")
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
ev("GAME.aim = { x: Math.floor(bot.x), y: 1 }")
pg.keyboard.press('f'); settle(100)
check('F away from usable things opens Vacuum', ev("!$('m-dialog').hidden && $('dlg-title').textContent === 'Vacuum'"))
ev("closeModal('m-dialog')")
# zoom jumps straight to the bot
pg.mouse.move(640, 400); settle(600)
pg.mouse.wheel(0, -100); pg.wait_for_timeout(60)
@@ -359,32 +337,49 @@ with sync_playwright() as p:
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
check('clicking a bag name chooses one blow material', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
pick_before = ev('[...GAME.collect]')
pg.click(f'#baglist [data-blow="{ev("CUORE")}"]'); settle(100)
ev('GAME.blow = new Set([RSTONE]); hudDirty = true')
pg.click('#selcard [data-vac-menu]'); settle(80)
pg.click(f'#dlg-body [data-vm-blow="{ev("COAL")}"]'); settle(80)
pg.click(f'#dlg-body [data-vm-blow="{ev("RSTONE")}"]'); settle(80)
pg.click(f'#dlg-body [data-vm-blow="{ev("CUORE")}"]'); settle(80)
pg.get_by_role('button', name='Done', exact=True).click(); settle(100)
check('blow marks make a mix without changing pick-up marks', ev('GAME.blow.has(COAL) && GAME.blow.has(CUORE) && GAME.blow.size === 2') and ev('[...GAME.collect]') == pick_before and 'Mix of 2' in pg.inner_text('#selcard'))
blow_before = ev('[...GAME.blow]')
pg.click(f'#baglist [data-pick="{ev("RSTONE")}"]'); settle(80)
pg.click('#selcard [data-vac-menu]'); settle(80)
check('card opens vacuum menu', ev("!$('m-dialog').hidden"))
pg.click(f'#dlg-body [data-vm-pick="{ev("RSTONE")}"]'); settle(80)
check('menu pick button marks material', ev('GAME.collect.has(RSTONE)'))
pg.get_by_role('button', name='Pick up everything', exact=True).click(); settle(80)
check('Pick up everything clears marks and keeps menu open', ev("!GAME.collect.size && !$('m-dialog').hidden"))
pg.get_by_role('button', name='Done', exact=True).click(); settle(80)
check('pick-up marks do not change blow marks', ev('[...GAME.blow]') == blow_before)
check('vacuum bag rows have both toggles', pg.locator('#baglist [data-pick]').count() == pg.locator('#baglist .bagrow').count() == pg.locator('#baglist [data-blow]').count())
check('vacuum bag has no pick or blow buttons', pg.locator('#baglist [data-pick], #baglist [data-blow]').count() == 0)
pg.mouse.move(mx_, my_); pg.mouse.down(button='right'); settle(500); pg.mouse.up(button='right'); settle(600)
dc, do_ = 60 - ev('bag[COAL]'), 60 - ev('bag[CUORE]')
check('right click blows an even marked ore/coal mix', dc > 5 and abs(dc - do_) <= 2, f'coal {dc}, ore {do_}')
pg.keyboard.press('e'); settle(80)
check('Q/E replaces the mix with one material', ev('GAME.blow.size === 1'))
ev("GAME.collect = new Set([COAL, CUORE]); setPickMode('marked'); GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
ev("GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL, RSTONE]); GAME.status = ''; lastBagKey = ''; hudDirty = true")
refill('bag[COAL] = 120; bag[CUORE] = 100; bag[RSTONE] = 600')
for width, height, suffix in [(1280, 800, ''), (900, 700, '-900')]:
pg.set_viewport_size({'width': width, 'height': height}); settle(200)
pg.screenshot(path=str(out / f'vacuum-card{suffix}.png'))
check(f'vacuum panel fits at {width}', ev("(() => { const c = $('selcard'), b = $('bagpanel'); return [c, b].every(e => { const r = e.getBoundingClientRect(); return r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight && e.scrollWidth <= e.clientWidth; }) && [...$('baglist').querySelectorAll('.bagrow')].every(e => e.scrollWidth <= e.clientWidth); })()"))
pg.click('#selcard [data-vac-menu]'); settle(80)
pg.screenshot(path=str(out / f'vacuum-menu{suffix}.png'))
check(f'vacuum menu fits at {width}', ev("(() => { const e = $('dlg-body'), r = e.closest('.plate').getBoundingClientRect(); return e.scrollWidth <= e.clientWidth && document.querySelector('.vpick').scrollWidth <= document.querySelector('.vpick').clientWidth && r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight; })()"))
pg.get_by_role('button', name='Blow one at a time', exact=True).click(); settle(80)
check(f'Blow one at a time keeps menu open at {width}', ev("GAME.blow.size === 1 && !$('m-dialog').hidden"))
pg.get_by_role('button', name='Done', exact=True).click(); settle(80)
pg.set_viewport_size({'width': 1280, 'height': 800})
pg.keyboard.press('3'); settle(80)
check('build bag has no vacuum toggles', pg.locator('#baglist .mark').count() == 0)
pg.keyboard.press('2'); settle(80)
ev("toggleBlow(BARCU)")
ev("GAME.blow = new Set([COAL, CUORE]); toggleBlow(BARCU)")
check('a bar blow mark replaces the mix', ev('GAME.blow.size === 1 && GAME.blow.has(BARCU)'))
ev("toggleBlow(COAL)")
check('a grain blow mark replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
ev("GAME.collect.clear(); setPickMode('first'); lastBagKey = ''; hudDirty = true")
ev("GAME.collect.clear(); lastBagKey = ''; hudDirty = true")
refill('bag[COAL] = 120')
# 4. igniter lights it
@@ -510,7 +505,13 @@ with sync_playwright() as p:
pg.keyboard.press('2'); settle(60)
ev("GAME.status = ''; GAME.statusT = 0"); n_toasts = ev("$('toasts').children.length")
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150); pg.keyboard.press('f'); settle(150)
check('F on an ingot mold says nothing', ev('GAME.status') == '' and ev("$('toasts').children.length") == n_toasts, ev('GAME.status'))
check('F on an automatic ingot mold opens vacuum menu without a toast', ev("!$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts)
ev("closeModal('m-dialog')")
check('F with vacuum uses what is in reach before opening the menu', ev("""(() => {
const old = interactAt; interactAt = () => 'Gate opened.';
try { doInteract(); } finally { interactAt = old; }
return $('m-dialog').hidden && GAME.status === 'Gate opened.';
})()"""))
settle(1500)
pg.screenshot(path=str(out / '06-bars.png'))
pg.keyboard.press('2'); settle(60)
@@ -724,10 +725,8 @@ with sync_playwright() as p:
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
ev("GAME.collect.clear(); setPickMode('first'); GAME.aim = { x: Math.floor(bot.x), y: Math.floor(bot.y) }; setCell(Math.floor(bot.y)*W+Math.floor(bot.x), COAL, 20, 0); __pad.buttons[2].pressed = true; pollPad(); __pad.buttons[2].pressed = false; pollPad()")
check('gamepad X keeps R pick-up marking', ev("GAME.collect.has(COAL) && GAME.pickMode === 'marked'"))
ev("__pad.buttons[1].pressed = true; pollPad(); __pad.buttons[1].pressed = false; pollPad()")
check('gamepad B cycles pick-up mode', ev("GAME.pickMode === 'all'"))
ev("GAME.collect.clear(); GAME.aim = { x: Math.floor(bot.x), y: Math.floor(bot.y) }; setCell(Math.floor(bot.y)*W+Math.floor(bot.x), COAL, 20, 0); __pad.buttons[2].pressed = true; pollPad(); __pad.buttons[2].pressed = false; pollPad()")
check('gamepad X keeps R pick-up marking', ev("GAME.collect.has(COAL)"))
ev("__pad.buttons[15].pressed = true; pollPad(); __pad.buttons[15].pressed = false; pollPad()")
check('gamepad d-pad chooses a single blow material', ev('GAME.blow.size === 1'))