Wall drags at 45° lay slopes; diagonal tubes drawn as one pipe; no side arrow on drag ghosts (t35, t36)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 22:20:48 -04:00
co-authored by Claude Opus 5.5
parent f19facbe5c
commit 814b51df46
17 changed files with 172 additions and 65 deletions
BIN
View File
Binary file not shown.
+14 -6
View File
@@ -677,14 +677,17 @@ with sync_playwright() as p:
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
# Slopes use the same mouse drag path as walls, with T for their corner.
pg.click('[data-piece="slope"]'); settle(60)
check('Sloped wall is next to Wall in the build menu', ev("piece().id === 'slope' && BUILD_CATS[0][1].slice(0,2).join(',') === 'wall,slope'"))
ev('GAME.wallMat = RSTONE; GAME.pieceDir.slope = 0')
pg.click('[data-piece="wall"]'); settle(60)
check('Wall replaces the Sloped wall menu piece', ev("piece().id === 'wall' && !BUILD_ORDER.includes('slope') && !document.querySelector('[data-piece=\"slope\"]')"))
ev('GAME.wallMat = RSTONE; GAME.pieceDir.wall = 0')
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('diagonal slope drag places four oriented blocks', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
pg.keyboard.press('Control+z'); settle(60)
pg.keyboard.press('t'); settle(60)
check('T cycles the single slope corner', ev('GAME.pieceDir.slope === 1'))
check('T cycles the single slope corner', ev('GAME.pieceDir.wall === 1 && pieceCode(piece()) === BP_SLOPE_BL'))
for shape in [2, 3, 4, 0]:
pg.keyboard.press('t'); settle(40)
check(f'T cycles Wall shape {shape}', ev(f'GAME.pieceDir.wall === {shape} && pieceCode(piece()) === ' + ('BP_WALL' if shape == 0 else f'BP_SLOPE_BL+{shape}-1')))
pg.mouse.move(*blk(bbx - 5, bby - 12)); settle(60); pg.mouse.down()
pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(60)
pg.keyboard.press('t'); settle(60); pg.mouse.up(); settle(60)
@@ -692,7 +695,7 @@ with sync_playwright() as p:
pg.keyboard.press('Control+z'); settle(60)
pg.click('[data-piece="wall"]'); settle(60)
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('square wall drag can also place a diagonal', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_WALL)'))
check('Wall diagonal defaults back to solid below', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
pg.keyboard.press('Control+z'); settle(60)
# Diagonal tubes use real mouse placement, preview arrows, R and picking.
@@ -732,12 +735,17 @@ with sync_playwright() as p:
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
pg.keyboard.press('2'); settle(60)
ev("GAME.status = ''; GAME.statusT = 0")
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150)
# Keep the camera still while aiming at a mold only four grains tall.
lead = ev('SET.cameraLead')
ev('SET.cameraLead = false; snapCamera()'); settle(100)
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 8, ev('molds[0].y') + 2)); settle(150)
check('mouse aims at the automatic ingot mold', ev('moldAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) === molds[0]'))
# count toasts made by the key press itself: counting the ones on screen also caught an older toast expiring
ev("window._toastsMade = []; window._toastFn = toast; toast = function (t, o) { _toastsMade.push(t); return _toastFn(t, o); }; 0")
pg.keyboard.press('f'); settle(150)
made = ev("(() => { toast = _toastFn; return _toastsMade; })()")
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and made == [], made)
ev(f'SET.cameraLead = {json.dumps(lead)}')
ev("closeModal('m-dialog')")
check('F with vacuum still uses what is in reach', ev("""(() => {
$('toasts').innerHTML = '';
+14
View File
@@ -168,4 +168,18 @@ for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
}
// The drawing can bridge a joint only in unoccupied air; transport parts stay square.
run(fs.readFileSync('src/render/world.js', 'utf8'));
for (const v of [4,5,6,7]) {
flatWorld(run,240,192,176);
const rising = v===4 || v===6;
run(`placeBlock(BP_TUBE,${v},20,20,true);placeBlock(BP_TUBE,${v},21,${rising ? 19 : 21},true);`);
const x=84, y=rising ? 80 : 83;
check(`diagonal ${v} draws over the empty corner joint`,run(`diagonalTubeBlock(${x},${y},false)>=0 && part[${y}*W+${x}]===0`));
for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) {
check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalTubeBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`));
}
run(`removeBlock(21,${rising ? 19 : 21});`);
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalTubeBlock(${x},${y},false)===-1`));
}
done();
+31 -4
View File
@@ -2,13 +2,19 @@
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
const { run } = load();
const { check, done } = suite();
// Wall selection drives the same saved slope codes and sealed funnel geometry.
const fs = require('fs');
run(fs.readFileSync('src/ui/selection.js', 'utf8'));
run(`const GAME = { pieceDir: {}, drag: null }; let hudDirty = false;
function aimBlock() { return { bx: 23, by: 23 }; }`);
function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); }
function funnel(sloped, material) {
fresh();
run(`for (let k=0;k<12;k++) {
placeBlock(${sloped ? 'BP_SLOPE_BL' : 'BP_WALL'}, RSTONE, 12+k, 12+k, true);
placeBlock(${sloped ? 'BP_SLOPE_BR' : 'BP_WALL'}, RSTONE, 36-k, 12+k, true);
}`);
run(`for (const [ax,ay,bx,by] of [[12,12,23,23],[36,12,25,23]]) {
GAME.drag={bx:ax,by:ay};
const code=${sloped ? 'pieceCode(PIECE_BY_ID.wall,{bx,by})' : 'BP_WALL'};
for(const [x,y] of blockLine(ax,ay,bx,by,true)) placeBlock(code,RSTONE,x,y,true);
} GAME.drag=null;`);
rect(run, 80, 20, 119, 29, material);
ticks(run, 1800);
const out = count(run, material, 0, 96, 239, 175);
@@ -87,4 +93,25 @@ run('placeBlock(BP_SLOPE_BL,RSTONE,20,20,true);placeBlock(BP_WALL,RSTONE,20,21,t
check('removing an adjoining square wall restores the slope seal',run('mat[slopeSeal(BP_SLOPE_BL,20,20)]===STONE'));
run('removeBlock(20,20)');
check('removal leaves no free seal grains behind',count(run,run('STONE'),80,80,83,87)===0);
check('old saved slope codes select Wall', run("Array.from({length:4},(_,k)=>PIECE_BY_CODE[15+k].id).every(id=>id==='wall')"));
check('single Wall starts square', run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
const shapes = [];
for (let k=0;k<5;k++) { run('turnSlope()'); shapes.push(run('pieceCode(PIECE_BY_ID.wall)')); }
check('T cycles square then four corners then square', JSON.stringify(shapes)===JSON.stringify([15,16,17,18,1]), shapes);
run('GAME.drag={bx:20,by:20}');
check('diagonal Wall defaults below', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_BL'));
run('turnSlope()');
check('T flips diagonal Wall above', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_TR'));
run('GAME.drag=null');
check('drag flip preserves single square choice',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
run('GAME.pieceDir.wall=3;GAME.drag={bx:20,by:23}');
check('straight Wall drag stays square even with a corner selected',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
run('GAME.drag=null;GAME.pieceDir.wall=1');
check('single Wall slope shows its half-block price',run('pieceCostNow(PIECE_BY_ID.wall,RSTONE)[0][1]===8'));
// Fixture saved before Wall absorbed the separate Sloped wall piece (save version 2).
const legacy = JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
globalThis.legacySlopeSave = legacy;
run('deserializeWorld(legacySlopeSave.meta,legacySlopeSave.buffers)');
check('old save loads all four slope corners unchanged',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10 && PIECE_BY_CODE[blockP[b]].id===\'wall\';}).every(Boolean)'));
delete globalThis.legacySlopeSave;
done();