SOURCE / PINNED RELEASE
Made of little things.
Napcraft
- Release
- ee47ac6a25df…
- Author-recorded commit
- 410dea87e109…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-52f9e22f5ce
Archive hash verified: 9e6e0cabb18d0d46…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { mkdir, writeFile } from 'node:fs/promises';
import { fitFrame } from './browser-support.mjs';
export async function blockPoint(frame,x,z,y=3,face='top') {
return frame.evaluate(({x,z,y,face})=>{
const c=document.querySelector('canvas'),w=c.clientWidth,h=c.clientHeight;
const u=Math.max(10,Math.min(w<600?w/26:w/43,h/29,33));
const px=face==='side'?x+1:x+.5, pz=z+.5, py=face==='side'?y-.5:y;
return {x:w/2+(px-pz)*u,y:h*.48+(px+pz-17)*u*.51-py*u*.94};
},{x,z,y,face});
}
const edits=p=>p.frame.locator('canvas').getAttribute('data-edits').then(Number);
const synced=(p,n)=>p.frame.waitForFunction(n=>Number(document.querySelector('canvas').dataset.confirmedEdits)===n&&document.querySelector('#app').dataset.pending==='0',n);
export default async ({players,connectIdentity,approveBackendAccount,check})=>{
const [a,b]=players;
await mkdir('.napplet-space/promotion',{recursive:true});
for(const p of players) await p.frame.evaluate(()=>{
const original=CanvasRenderingContext2D.prototype.fillText;
window.drawnLabels=[];
CanvasRenderingContext2D.prototype.fillText=function(text,...args){
window.drawnLabels.push({text,at:performance.now()});
if(window.drawnLabels.length>100)window.drawnLabels.shift();
return original.call(this,text,...args);
};
});
await connectIdentity(a);
await a.frame.locator('#create').click();await a.frame.locator('#confirmCreate').click();
await approveBackendAccount(a,'worlds');await a.frame.locator('#share').waitFor();
await a.frame.locator('#share').click();const code=await a.frame.locator('#shareCode').inputValue();await a.frame.locator('#close').click();
const join=async p=>{await p.frame.locator('#join').click();await p.frame.locator('#code').fill(code);await p.frame.locator('#confirmJoin').click();await p.frame.locator('#share').waitFor();};
await join(b);
await Promise.all(players.map(p=>p.frame.waitForFunction(()=>document.querySelector('canvas').dataset.peerCount==='1')));
await a.page.waitForTimeout(400);
for(const [i,p] of players.entries()) {
const visible=await p.frame.evaluate(()=>{
const c=document.querySelector('canvas'),peer=JSON.parse(c.dataset.peerPositions)[0];
return {distinct:Math.hypot(Number(c.dataset.localX)-peer[1],Number(c.dataset.localZ)-peer[2])>=1.1,
label:window.drawnLabels.some(p=>p.text.startsWith('FRIEND ')&&performance.now()-p.at<100)};
});
check(`player ${i+1} sees a distinct gardener and rendered friend label`,visible.distinct&&visible.label);
}
const before=Number(await b.frame.locator('canvas').getAttribute('data-local-x'));
await b.frame.locator('canvas').focus();await b.page.keyboard.down('ArrowRight');await b.page.waitForTimeout(400);await b.page.keyboard.up('ArrowRight');
await a.frame.waitForFunction(x=>JSON.parse(document.querySelector('canvas').dataset.peerPositions)[0][1]>x+.5,before);
check('watcher renders friend movement',true);
const point=await blockPoint(b.frame,2,8);
await b.frame.locator('canvas').hover({position:point});
await b.frame.waitForFunction(()=>document.querySelector('canvas').dataset.selected==='2,8');
check('hover previews column without editing',await edits(b)===0);
await b.frame.locator('canvas').click({position:point});
await Promise.all(players.map(p=>synced(p,1)));
check('one desktop click places one persistent shared block',true);
const side=await blockPoint(b.frame,2,8,4,'side');
await b.frame.locator('canvas').click({position:side,button:'right'});
await Promise.all(players.map(p=>synced(p,2)));
check('right-click on a visible side erases its column top',true);
await b.frame.locator('canvas').click({position:point,modifiers:['Shift']});
await b.page.waitForTimeout(150);
check('Shift-click aims without placing',await edits(b)===2);
await b.frame.locator('canvas').click({position:{x:160,y:145}});
check('clicking empty sky does not build',await edits(b)===2);
const box=await b.frame.locator('canvas').boundingBox();
await b.page.mouse.move(box.x+point.x,box.y+point.y);await b.page.mouse.down();
await b.page.mouse.move(box.x+point.x+45,box.y+point.y+25,{steps:5});await b.page.mouse.up();
await b.page.waitForTimeout(100);
check('drag starting on a block pans without an edit',await edits(b)===2);
await b.page.mouse.down();await b.page.mouse.move(box.x+point.x,box.y+point.y,{steps:5});await b.page.mouse.up();
await b.page.mouse.move(box.x+point.x,box.y+point.y);await b.page.mouse.down();
await b.frame.evaluate(({x,y})=>{
const c=document.querySelector('canvas');
// Cancellation is a browser/device lifecycle event, not another edit.
c.dispatchEvent(new PointerEvent('pointercancel',{pointerId:1,bubbles:true,clientX:x,clientY:y}));
},point);
await b.page.mouse.up();
await b.frame.locator('canvas').click({position:point,button:'middle'});
check('cancel and middle click do not edit',await edits(b)===2);
await fitFrame(b.page,390,844);
const cdp=await b.page.context().newCDPSession(b.page);
await cdp.send('Emulation.setTouchEmulationEnabled',{enabled:true});
const mobilePoint=await blockPoint(b.frame,3,8);
const mobileBox=await b.frame.locator('canvas').boundingBox();
const tap=async(x,y)=>{await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x,y}]});await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});};
await tap(mobileBox.x+mobilePoint.x,mobileBox.y+mobilePoint.y);
await b.frame.waitForFunction(()=>document.querySelector('canvas').dataset.selected==='3,8');
check('touch tap aims without accidental placement',await edits(b)===2);
const build=await b.frame.locator('#place').boundingBox();await tap(build.x+build.width/2,build.y+build.height/2);
await Promise.all(players.map(p=>synced(p,3)));
check('touch Build saves exactly one block after aiming',true);
await b.page.waitForTimeout(500);
check('live player indicator is visible on mobile',await b.frame.locator('#presence').isVisible()&&(await b.frame.locator('#presence').innerText()).includes('2 building'));
await a.page.locator('#stage iframe').screenshot({path:'.napplet-space/promotion/friends-desktop.png'});
await b.page.locator('#stage iframe').screenshot({path:'.napplet-space/promotion/friends-touch.png'});
await cdp.detach();
await b.page.reload();
await a.frame.waitForFunction(()=>document.querySelector('canvas').dataset.peerCount==='0');
check('departed player marker disappears',true);
b.frame=await(await b.page.locator('#stage iframe').elementHandle()).contentFrame();await b.frame.locator('#app[data-ready=true]').waitFor();await join(b);
await a.frame.waitForFunction(()=>document.querySelector('canvas').dataset.peerCount==='1');
await synced(b,3);
check('rejoined player appears again and saved blocks persist',true);
await writeFile('.napplet-space/direct-building.json',JSON.stringify({passed:true,edits:3,players:2},null,2));
};
