Back to Napcraft
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.

tests/direct-building.mjs
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));
};