Far sidewall drawn behind the load with cleat ribs at their physics positions; sloped screens get a tinted mesh between rails and shake only when loaded. Powder skips cleat checks outside chunks that have cleats, and empty slopes let their chunks sleep. Record the screw conveyor and stall/carry-depth decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
7.3 KiB
Python
112 lines
7.3 KiB
Python
"""The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works."""
|
|
import sys, pathlib
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
|
from use_venv import ensure_playwright; ensure_playwright()
|
|
from playwright.sync_api import sync_playwright
|
|
root = pathlib.Path(__file__).resolve().parent.parent
|
|
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'smooth'); out.mkdir(parents=True, exist_ok=True)
|
|
fails = []
|
|
def check(name, cond, extra=''):
|
|
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
|
|
if not cond: fails.append(name)
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
|
|
pg = b.new_page(viewport={'width': 1024, 'height': 640})
|
|
logs = []
|
|
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 in ('error', 'warning') else None)
|
|
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
|
|
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
|
|
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
|
pg.wait_for_function('GAME.running === true', timeout=20000); pg.wait_for_timeout(1500)
|
|
ev = pg.evaluate
|
|
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && SM.ok && R.smoothOn"), ev('[SET.look, SM.ok, SM.why, R.smoothOn]'))
|
|
# the canvas shows the GPU image: sample a sky pixel and a ground pixel, they must differ and not be black
|
|
px = ev("""(() => { const c = R.ctx.getImageData(R.canvas.width >> 1, 4, 1, 1).data, g = worldToScreenPx(bot.x + 2, surfY[Math.floor(bot.x)] + 6), d = R.ctx.getImageData(Math.round(g.x), Math.round(g.y), 1, 1).data; return [Array.from(c), Array.from(d)]; })()""")
|
|
check('it draws sky and ground', sum(px[0][:3]) > 200 and px[0][:3] != px[1][:3], px)
|
|
pg.screenshot(path=str(out / 'smooth.png'))
|
|
ev("player.lens = true"); pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
|
check('the thermal lens draws in pixels', ev('R.lens && !R.smoothOn'))
|
|
pg.keyboard.press('h'); pg.wait_for_timeout(1500)
|
|
check('and the smooth look comes back after it', ev('!R.lens && R.smoothOn'), ev('[R.lens, R.smoothOn, SET.look, SM.ok, SM.why, SM.ms, SM.scale]'))
|
|
ev("SET.look = 'pixel'"); pg.wait_for_timeout(1500)
|
|
check('the pixel look can be chosen', ev('!R.smoothOn'))
|
|
pg.screenshot(path=str(out / 'pixel.png'))
|
|
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
|
|
ev("SM.scale = 0.5"); pg.wait_for_timeout(1500)
|
|
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
|
|
# Uniform colours isolate the solid/loose seam from material noise and lighting.
|
|
ground = ev('''() => {
|
|
GAME.paused = true; requestAnimationFrame = () => 0;
|
|
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = 0; SET.zoom = 6;
|
|
resizeRender(); bot.x = 138; bot.y = 118; snapCamera();
|
|
for (let y = 60; y < 220; y++) for (let x = 40; x < 240; x++) {
|
|
const i = y * W + x;
|
|
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
|
|
}
|
|
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
|
|
const at = (x, y) => { const c = R.px[(y - R.vy0) * R.bw + x - R.vx0]; return (c & 255) + (c >>> 8 & 255) + (c >>> 16 & 255); };
|
|
const out = {};
|
|
for (const lens of [false, true]) {
|
|
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
|
|
const edge = at(140, 140), inside = at(140, 142), loose = at(140, 130);
|
|
fill(STONE); drawWorld();
|
|
out[lens ? 'thermal' : 'pixel'] = [edge, inside, loose, at(140, 140)];
|
|
}
|
|
R.lens = false;
|
|
const i = 140 * W + 140;
|
|
flags[i] = 0; fill(RSTONE); drawWorld(); const built = at(140, 140);
|
|
fill(STONE); drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
|
|
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
|
|
const sample = (x, y) => {
|
|
const p = worldToScreenPx(x, y);
|
|
const c = R.ctx.getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
|
|
return c[0] + c[1] + c[2];
|
|
};
|
|
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
|
|
const packed = [];
|
|
for (let y = 120; y < 121; y += 1 / 6) for (let x = 140; x < 141; x += 1 / 6) packed.push(sample(x, y));
|
|
out.texture = Math.max(...packed) - Math.min(...packed);
|
|
return out;
|
|
}''')
|
|
for look in ['pixel', 'thermal']:
|
|
edge, inside, loose, solid = ground[look]
|
|
check(f'{look}: ground has a darker edge against loose grains', edge < inside * 0.85 and inside == solid, ground[look])
|
|
check(f'{look}: loose rubble is lighter than solid stone', loose > inside, ground[look])
|
|
check('built solids get no loose-grain outline', ground['built'][0] == ground['built'][1], ground['built'])
|
|
check('smooth: a thin darker seam separates ground and rubble', ground['smooth'][0] < ground['smooth'][1] * 0.9, ground['smooth'])
|
|
check('smooth: packed rubble keeps pebble shading', ground['texture'] > 30, ground['texture'])
|
|
wall = pg.evaluate('''() => {
|
|
GAME.paused = true; R.lens = false; SET.fog = false;
|
|
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = false;
|
|
for(let y=20;y<150;y++)for(let x=40;x<240;x++)setCell(y*W+x,EMPTY,22,0);
|
|
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,25+k,25-k,true);
|
|
rebuildPower();slopeCleatsTick(false);prepareSlopeBelts(40,20,240,150);
|
|
let cell=-1;
|
|
for(let y=40;y<100 && cell<0;y++)for(let x=100;x<124;x++)
|
|
if(slopeDetailAt(y*W+x,x,y)===2){cell=y*W+x;break;}
|
|
if(cell<0)return null;
|
|
const x=cell%W,y=(cell/W)|0,control=80*W+180;
|
|
bot.x=118;bot.y=78;snapCamera();
|
|
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
|
|
noiseB[cell]=noiseB[control]=128;
|
|
const at=i=>R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0];
|
|
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
|
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
|
|
return {colors,grainClass};
|
|
}''')
|
|
check('sidewall stays behind the grain colour in pixels and smooth',
|
|
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
|
|
# a browser without hardware drawing gets the pixel look and a note
|
|
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
|
pg2.add_init_script('localStorage.clear();')
|
|
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
|
|
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
|
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
|
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
|
errs = [l for l in logs if 'PAGEERROR' in l or l.startswith('error')]
|
|
check('no errors', not errs, errs[:3])
|
|
b.close()
|
|
print('FAILS:', fails)
|
|
sys.exit(1 if fails else 0)
|