Vacuum: one selection for pick-up and blow, full list while Ctrl is held (t20)
With anything selected a drag takes only the selection; with nothing selected it takes what you click. Right click blows the selected materials in the bag. Holding Ctrl lists every material found so far under "Not in bag". Selections stay when materials run out; Clear button, clicking the only selected row, or gamepad B clears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+50
-21
@@ -228,34 +228,38 @@ with sync_playwright() as p:
|
||||
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}")
|
||||
# Drive one tick at a time so the pile cannot fall between the click and pickup.
|
||||
ev("""window.vacCase = (mode, hand = []) => {
|
||||
ev("""window.vacCase = (mode, selection = []) => {
|
||||
const x = Math.floor(bot.x) + 6, y = Math.floor(bot.y) - 6, cells = [], aim = GAME.aim, blow = GAME.blow;
|
||||
const oldBag = bag.slice(), total = bagTotal, flyers0 = flyers.length;
|
||||
for (let dy = -6; dy <= 6; dy++) for (let dx = -6; dx <= 6; dx++) { const i = (y+dy)*W+x+dx; cells.push([i, mat[i], temp[i], flags[i]]); setCell(i, EMPTY, 20, 0); }
|
||||
bag.fill(0); bagTotal = 0; GAME.blow = new Set(hand); GAME.aim = { x, y }; GAME.lmb = true;
|
||||
bag.fill(0); bagTotal = 0; GAME.blow = new Set(selection); GAME.aim = { x, y }; GAME.lmb = true;
|
||||
if (mode !== 'air') setCell(y*W+x+1, LDIRT, 20, 0);
|
||||
if (mode !== 'air') setCell(y*W+x, CUORE, 20, 0);
|
||||
if (mode !== 'air') setCell(y*W+x, mode === 'dirt' ? LDIRT : CUORE, 20, 0);
|
||||
if (selection.length) { setCell(y*W+x+2, COAL, 20, 0); setCell(y*W+x+3, CUORE, 20, 0); }
|
||||
primaryPress(mode === 'shift');
|
||||
let waiting = true;
|
||||
if (mode === 'air') { toolTick(); waiting = GAME.vacFilter === 0 && bagTotal === 0; setCell(y*W+x, CUORE, 20, 0); setCell(y*W+x+1, LDIRT, 20, 0); }
|
||||
if (mode === 'air') { toolTick(); waiting = selection.length ? GAME.vacFilter === -2 : GAME.vacFilter === 0 && bagTotal === 0; setCell(y*W+x, CUORE, 20, 0); setCell(y*W+x+1, LDIRT, 20, 0); }
|
||||
toolTick();
|
||||
if (selection.length) { GAME.aim = { x: x + 2, y }; toolTick(); }
|
||||
if (mode === 'ore') { GAME.aim = { x: x + 1, y }; toolTick(); }
|
||||
const result = { ore: bag[CUORE], dirt: bag[LDIRT], dirtStays: mat[y*W+x+1] === LDIRT, filter: GAME.vacFilter, hand: [...GAME.blow], waiting };
|
||||
const result = { coal: bag[COAL], ore: bag[CUORE], dirt: bag[LDIRT], dirtStays: mat[y*W+x+1] === LDIRT, filter: GAME.vacFilter, selection: [...GAME.blow], waiting };
|
||||
primaryRelease(false); GAME.lmb = false; GAME.aim = aim; GAME.blow = blow;
|
||||
for (const [i, m, t, f] of cells) setCell(i, m, t, f);
|
||||
bag.set(oldBag); bagTotal = total; flyers.length = flyers0; return result;
|
||||
}""")
|
||||
r = ev("vacCase('ore')")
|
||||
check('drag starting on ore takes only ore and leaves dirt', r['ore'] > 0 and r['dirt'] == 0 and r['dirtStays'] and r['filter'] == ev('CUORE'))
|
||||
check('pickup puts ore in an empty hand', r['hand'] == [ev('CUORE')])
|
||||
check('pickup leaves the selection empty', r['selection'] == [])
|
||||
r = ev("vacCase('air')")
|
||||
check('drag starting in air waits and locks onto the first loose grain', r['waiting'] and r['ore'] > 0 and r['dirt'] == 0 and r['filter'] == ev('CUORE'))
|
||||
r = ev("vacCase('shift')")
|
||||
check('Shift drag takes everything and puts the first pickup in hand', r['ore'] > 0 and r['dirt'] > 0 and r['filter'] == -1 and r['hand'] == [ev('CUORE')])
|
||||
check('picking up ore keeps coal in hand', ev("vacCase('ore', [COAL]).hand.join() === String(COAL)"))
|
||||
check('picking up ore keeps a chosen mix', ev("vacCase('ore', [COAL, RSTONE]).hand.join() === [COAL, RSTONE].join()"))
|
||||
check('selecting Vacuum leaves an empty hand empty', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); selectTool(0); selectTool(1); renderSelCard(); const ok = !GAME.blow.size && $('selcard').querySelector('.vacchoice').textContent === 'empty' && $('selcard').querySelector('.num.chip').textContent === 'none'; GAME.blow = old; return ok; })()"))
|
||||
check('Q/E from an empty hand chooses the first or last bag material', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); cycleItem(1); const first = blowList()[0] === bagMaterials()[0]; GAME.blow.clear(); cycleItem(-1); const last = blowList()[0] === bagMaterials().at(-1); GAME.blow = old; return first && last; })()"))
|
||||
r = ev("vacCase('shift', [COAL])")
|
||||
check('Shift drag takes everything and keeps the selection', r['ore'] > 0 and r['dirt'] > 0 and r['filter'] == -1 and r['selection'] == [ev('COAL')])
|
||||
for mode in ['dirt', 'air']:
|
||||
r = ev(f"vacCase('{mode}', [COAL, CUORE])")
|
||||
check(f'selected coal and ore are picked starting on {mode}, dirt stays', r['coal'] > 0 and r['ore'] > 0 and r['dirt'] == 0 and r['dirtStays'] and r['filter'] == -2 and r['selection'] == ev('[COAL, CUORE]'))
|
||||
check('selecting Vacuum leaves an empty selection empty', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); selectTool(0); selectTool(1); renderSelCard(); const ok = !GAME.blow.size && $('selcard').querySelector('.vacchoice').textContent === 'nothing selected' && $('selcard').querySelector('.num.chip').textContent === 'none'; GAME.blow = old; return ok; })()"))
|
||||
check('Q/E when selection is absent from the bag chooses first or last', ev("(() => { const old = GAME.blow, n = bag[SAND]; bag[SAND] = 0; GAME.blow = new Set([SAND]); cycleItem(-1); const last = blowList()[0] === bagMaterials().at(-1); GAME.blow = new Set([SAND]); cycleItem(1); const first = blowList()[0] === bagMaterials()[0]; bag[SAND] = n; GAME.blow = old; return first && last; })()"))
|
||||
check('Q/E from an empty selection chooses the first or last bag material', ev("(() => { const old = GAME.blow; GAME.blow = new Set(); cycleItem(1); const first = blowList()[0] === bagMaterials()[0]; GAME.blow.clear(); cycleItem(-1); const last = blowList()[0] === bagMaterials().at(-1); GAME.blow = old; return first && last; })()"))
|
||||
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;
|
||||
const old = [mat[i], temp[i], flags[i]], aim = GAME.aim, blow = GAME.blow, n = bag[LDIRT];
|
||||
@@ -265,6 +269,7 @@ with sync_playwright() as p:
|
||||
})()"""))
|
||||
pg.screenshot(path=str(out / '02-vacuum.png'))
|
||||
|
||||
ev('GAME.blow.clear(); hudDirty = true')
|
||||
# 2b. the vacuum filter stays fixed while dragging through a mixed pile
|
||||
def mixed_pile(x0, y0):
|
||||
ev(f"{{ const r = mulberry32(3); for (let y = {y0}; y < {y0} + 6; y++) for (let x = {x0}; x < {x0} + 14; x++) setCell(y*W+x, r() < 0.5 ? CUORE : RSTONE, AMB[y], 0); }}")
|
||||
@@ -313,7 +318,7 @@ with sync_playwright() as p:
|
||||
pg.mouse.wheel(0, 100); settle(300)
|
||||
refill('bag.fill(0)')
|
||||
|
||||
# 3. right button blows what is in hand; empty materials leave it
|
||||
# 3. right button blows the selection; exhausted materials stay selected
|
||||
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
||||
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
||||
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
||||
@@ -325,11 +330,13 @@ with sync_playwright() as p:
|
||||
settle(350); pg.mouse.up(button='right'); settle(800)
|
||||
check('right click blows coal out of the bag onto the ground', ev('bag[COAL]') < c0 and ev('flyers.length') == 0, f"{c0}->{ev('bag[COAL]')}")
|
||||
refill('bag[COAL] = 3')
|
||||
ev('GAME.blow.clear(); lastBagKey = ""; hudDirty = true'); settle(80)
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
hold(dx_, dy_, 400, 'right'); settle(400)
|
||||
check('blowing the last material empties the hand', ev('bag[COAL] === 0 && !GAME.blow.size'), ev('[bag[COAL], ...GAME.blow]'))
|
||||
check('empty hand blows nothing and explains how to choose', ev("(() => { const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; return bagTotal === n && flyers.length === f && GAME.status === 'Nothing in hand: Q/E or click something in the bag.'; })()"))
|
||||
check('a mix drops only the material that runs out', ev("(() => { const b = bag.slice(), total = bagTotal, f = flyers.length; bag.fill(0); bag[COAL] = 1; bag[CUORE] = 100; bagTotal = 101; GAME.blow = new Set([COAL, CUORE]); GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; const ok = !bag[COAL] && bag[CUORE] > 0 && GAME.blow.size === 1 && GAME.blow.has(CUORE); bag.set(b); bagTotal = total; flyers.length = f; return ok; })()"))
|
||||
check('blowing the last coal keeps it selected', ev('bag[COAL] === 0 && GAME.blow.has(COAL)'))
|
||||
check('selected but none in bag blows nothing', ev("(() => { const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; renderSelCard(); return bagTotal === n && flyers.length === f && GAME.status === 'None of the selected materials are in the bag.' && $('selcard').querySelector('.vacchoice').textContent === 'none in bag'; })()"))
|
||||
check('nothing selected blows nothing and explains how to choose', ev("(() => { GAME.blow.clear(); const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; return bagTotal === n && flyers.length === f && GAME.status === 'Nothing selected: click or Q/E in the bag.'; })()"))
|
||||
check('a mix keeps exhausted coal selected and continues blowing ore', ev("(() => { const b = bag.slice(), total = bagTotal, f = flyers.length; bag.fill(0); bag[COAL] = 1; bag[CUORE] = 100; bagTotal = 101; GAME.blow = new Set([COAL, CUORE]); GAME.rmb = true; secondaryPress(false); toolTick(); const ore = bag[CUORE]; toolTick(); GAME.rmb = false; const ok = !bag[COAL] && ore > 0 && bag[CUORE] < ore && GAME.blow.size === 2 && GAME.blow.has(COAL) && GAME.blow.has(CUORE); bag.set(b); bagTotal = total; flyers.length = f; return ok; })()"))
|
||||
# Shift + right click blows an even mix of everything loose in the bag, without changing the choice
|
||||
refill('bag[COAL] = 60; bag[CUORE] = 60')
|
||||
ev('GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
|
||||
@@ -348,7 +355,7 @@ with sync_playwright() as p:
|
||||
mx_, my_ = cell_to_screen(bx - 26, gy - 6)
|
||||
pg.mouse.move(mx_, my_); pg.keyboard.down('Shift'); pg.mouse.down(button='right'); settle(400); pg.mouse.up(button='right'); pg.keyboard.up('Shift'); settle(600)
|
||||
check('Shift + right click blows everything loose in the bag', ev('bag[CUORE]') < 60 and ev('bag[COAL]') < 60 and ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'), ev('[bag[COAL], bag[CUORE]]'))
|
||||
refill('bag[COAL] = 60; bag[CUORE] = 60'); settle(200)
|
||||
refill('bag[COAL] = 60; bag[CUORE] = 60'); ev('GAME.blow.clear(); lastBagKey = ""; hudDirty = true'); settle(200)
|
||||
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)'))
|
||||
ore_row = pg.locator('#baglist .pick').filter(has_text='Copper ore')
|
||||
@@ -357,9 +364,9 @@ with sync_playwright() as p:
|
||||
ore_row.click(modifiers=['Control']); settle(80)
|
||||
check('Ctrl+click again takes it out of the mix', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80)
|
||||
check('right click on a bag row leaves the hand alone', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
check('right click on a bag row leaves the selection alone', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
check('bag rows have no pickup tag', '⬇' not in pg.inner_text('#baglist'))
|
||||
check('bag hint describes the hand', pg.locator('#baglist .baghint').inner_text() == 'Click: put in hand · Ctrl+click: add to the mix · Q/E: next')
|
||||
check('bag hint describes the selection', "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next" in pg.locator('#baglist .baghint').inner_text())
|
||||
ore_row.click(modifiers=['Control']); settle(80)
|
||||
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)
|
||||
@@ -373,14 +380,33 @@ with sync_playwright() as p:
|
||||
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); })()"))
|
||||
ev("seen[SAND] = 1; bagTotal -= bag[SAND]; bag[SAND] = 0; GAME.blow = new Set([COAL]); lastBagKey = ''; renderBag(); window.bagPositions = [...$('baglist').querySelectorAll('.bagrow:not(.none)')].map(e => e.getBoundingClientRect().top); window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); renderBag()")
|
||||
check('Ctrl reveals known materials below stable in-bag rows', ev("""GAME.ctrl && !!$('baglist').querySelector('.bagsep') && !!$('baglist').querySelector('.bagrow.none .pick[title="Select Sand"]') && bagPositions.every((y, i) => y === $('baglist').querySelectorAll('.bagrow:not(.none)')[i].getBoundingClientRect().top)"""))
|
||||
pg.screenshot(path=str(out / 'vacuum-ctrl-900.png'))
|
||||
pg.locator('#baglist .none .pick[title="Select Sand"]').click(modifiers=['Control']); settle(80)
|
||||
check('Ctrl click selects a material not in the bag and keeps its row after release', ev("GAME.blow.has(SAND) && !GAME.ctrl && !!$('baglist').querySelector('.bagrow.none.on .pick[title=\"Select Sand\"]')"))
|
||||
check('drag picks the newly selected missing material', ev("(() => { const aim = GAME.aim, x = Math.floor(bot.x)+6, y = Math.floor(bot.y)-6, i = y*W+x, old = [mat[i],temp[i],flags[i]]; setCell(i,SAND,20,0); GAME.aim = {x,y}; GAME.lmb = true; primaryPress(false); toolTick(); primaryRelease(false); GAME.lmb = false; GAME.aim = aim; const ok = bag[SAND] > 0; setCell(i,...old); return ok; })()"))
|
||||
ev("GAME.blow.delete(SAND); bagTotal -= bag[SAND]; bag[SAND] = 0; window.dispatchEvent(new KeyboardEvent('keyup', {key:'Control'})); renderBag()")
|
||||
check('Ctrl release hides unselected missing materials', ev("""!GAME.ctrl && !$('baglist').querySelector('.bagrow.none .pick[title="Select Sand"]')"""))
|
||||
ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Meta', metaKey:true})); renderBag()")
|
||||
check('Meta reveals known materials too', ev("""GAME.ctrl && !!$('baglist').querySelector('.bagsep')"""))
|
||||
ev("window.dispatchEvent(new KeyboardEvent('keyup', {key:'Meta'})); renderBag()")
|
||||
check('Meta release hides unselected missing materials', ev('!GAME.ctrl'))
|
||||
ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); window.dispatchEvent(new Event('blur')); renderBag()")
|
||||
check('blur clears the modifier', ev('!GAME.ctrl'))
|
||||
pg.locator('[data-bag-clear]').click(); settle(80)
|
||||
check('Clear clears the selection', ev('!GAME.blow.size'))
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
check('plain click on the only selected row clears it', ev('!GAME.blow.size'))
|
||||
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("GAME.blow = new Set([COAL, CUORE]); toggleBlow(BARCU)")
|
||||
check('putting a bar in hand replaces the mix', ev('GAME.blow.size === 1 && GAME.blow.has(BARCU)'))
|
||||
check('selecting a bar replaces the mix', ev('GAME.blow.size === 1 && GAME.blow.has(BARCU)'))
|
||||
ev("toggleBlow(COAL)")
|
||||
check('putting a grain in hand replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
check('selecting a grain replaces a bar', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
|
||||
refill('bag[COAL] = 120')
|
||||
|
||||
# 4. igniter lights it
|
||||
@@ -516,6 +542,7 @@ with sync_playwright() as p:
|
||||
settle(1500)
|
||||
pg.screenshot(path=str(out / '06-bars.png'))
|
||||
pg.keyboard.press('2'); settle(60)
|
||||
ev('GAME.blow.clear(); lastBagKey = ""; hudDirty = true')
|
||||
e0 = ev('[ents[0].ix + 2, ents[0].iy + 1]')
|
||||
pg.mouse.move(*cell_to_screen(*e0)); settle(900)
|
||||
hold(*cell_to_screen(*e0), 900)
|
||||
@@ -726,6 +753,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.blow = new Set([COAL]); __pad.buttons[1].pressed = true; pollPad(); __pad.buttons[1].pressed = false; pollPad()")
|
||||
check('gamepad B clears the Vacuum selection', ev('!GAME.blow.size'))
|
||||
ev("selectTool(2); GAME.pieceIdx = PIECE_BY_ID.belt.index; GAME.pieceDir.belt = 0; __pad.buttons[2].pressed = true; pollPad(); __pad.buttons[2].pressed = false; pollPad()")
|
||||
check('gamepad X rotates with the Build tool', ev('GAME.pieceDir.belt === 1'))
|
||||
ev('selectTool(1)')
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ run('for (let k=0;k<5;k++) { toolBlow(80, 70, 1, COAL); simTick(); }');
|
||||
check('blown coal flies out of the bot', run('flyers.length') > 0, run('flyers.length'));
|
||||
ticks(run, 120);
|
||||
check('blown coal leaves the bag and lands on the ground', run('bag[COAL]') < c0 && run('flyers.length') === 0 && count(run, run('COAL'), 60, 80, 100, 99) > 0, `${c0}->${run('bag[COAL]')} landed ${count(run, run('COAL'), 60, 80, 100, 99)}`);
|
||||
check('empty blow mix explains that nothing is in hand', run('toolBlow(80, 70, 1, COAL, [CUORE, BARCU]).status') === 'Nothing in hand: Q/E or click something in the bag.');
|
||||
check('no selection explains how to select', run('toolBlow(80, 70, 1, 0).status') === 'Nothing selected: click or Q/E in the bag.');
|
||||
check('unavailable blow mix explains none are in the bag', run('toolBlow(80, 70, 1, COAL, [CUORE, BARCU]).status') === 'None of the selected materials are in the bag.');
|
||||
// bot flies through built walls but not through natural rock
|
||||
run('bag[RSTONE] += 64; bagTotal += 64; placeBlocks(BP_WALL, RSTONE, blockRect(30, 14, 30, 17))');
|
||||
check('bot can overlap a built wall', !run('botOverlapSolid(121, 58)'));
|
||||
|
||||
Reference in New Issue
Block a user