Files
deepgrain-foundry/test/t_smooth.py
T

164 lines
10 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)
# Edge joins keep closed elbows; corner placements match two isolated free ends.
pg.set_viewport_size({"width": 2100, "height": 1300})
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}")
mouths = ev('''() => {
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender();
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
const now=performance.now.bind(performance),frozen=now();
performance.now=()=>frozen;
try {
const out={};
const draw=()=>{SM.scale=1;drawWorld();};
for(const look of ["pixel","smooth"]) {
SET.look=look;draw();
const bad=[];
for(const j of T62_JOINTS) {
const slope=j.v===4 || j.v===6?-1:1;
const end=j.end*SCREW_DIRS[j.v][0];
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0;
if(j.corner)continue;
for(const side of [-1,1])for(const along of [-0.3,0.3]) {
const x=j.bx*4+2+dx*(2+along)-dy*side*1.6;
const y=j.by*4+2+dy*(2+along)+dx*side*1.6;
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data;
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]);
}
}
const sample=(bx,by)=>{
const colors=[];
for(let y=0;y<4;y++)for(let x=0;x<4;x++) {
const p=worldToScreenPx(bx*4+x+0.5,by*4+y+0.5);
colors.push(...R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);
}return colors.join(',');
};
for(const j of T62_JOINTS.filter(j=>j.corner)) {
const [dx,dy]=SCREW_DIRS[j.v],sx=j.bx+j.end*dx,sy=j.by+j.end*dy;
const collar=sample(j.bx,j.by),straight=sample(sx,sy);
const joins=diagonalScrewJoins(j.bx,j.by,j.v),end=j.end*dx>0?1:0;
if(!(joins & (64<<end)) || (joins>>(end*3)&7))bad.push(['missing collar',j]);
removeBlock(sx,sy);draw();
if(sample(j.bx,j.by)!==collar)bad.push(['diagonal differs from isolated end',j]);
placeBlock(BP_SCREW,j.straight,sx,sy,true);
removeBlock(j.bx,j.by);draw();
if(sample(sx,sy)!==straight)bad.push(['straight differs from normal end',j]);
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);draw();
}
out[look]=bad;
}return out;
} finally {performance.now=now;}
}''')
for look, bad in mouths.items():
check(f"{look}: edge elbows stay closed and corners show two separate free ends", not bad, bad[:2])
# 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)