Back to Napcraft
SOURCE / PINNED RELEASE

Made of little things.

Napcraft

Release
ee47ac6a25df…
Author-recorded commit
410dea87e109…
License
Check the original source
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-52f9e22f5ce

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,viewport-fit=cover"
    />
    <meta name="theme-color" content="#dedcf5" />
    <title>Napcraft</title>
    <script type="module" crossorigin>var ct=Object.defineProperty;var lt=(t,e,r)=>e in t?ct(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var m=(t,e,r)=>lt(t,typeof e!="symbol"?e+"":e,r);function dt(){const t=window;if(!t.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return t.napplet}function v(t){const r=dt()[t];if(!r)throw new Error(`window.napplet.${String(t)} is unavailable -- runtime did not inject this domain`);return r}var L={getItem(t){return v("storage").getItem(t)},setItem(t,e){return v("storage").setItem(t,e)},removeItem(t){return v("storage").removeItem(t)},keys(){return v("storage").keys()},instance:{getItem(t){return v("storage").instance.getItem(t)},setItem(t,e){return v("storage").instance.setItem(t,e)},removeItem(t){return v("storage").instance.removeItem(t)},keys(){return v("storage").instance.keys()}}},se={getPublicKey(){return v("identity").getPublicKey()},onChanged(t){return v("identity").onChanged(t)},getRelays(){return v("identity").getRelays()},getProfile(){return v("identity").getProfile()},getFollows(){return v("identity").getFollows()},getList(t){return v("identity").getList(t)},getZaps(){return v("identity").getZaps()},getMutes(){return v("identity").getMutes()},getBlocked(){return v("identity").getBlocked()},getBadges(){return v("identity").getBadges()}},Fe={discover(t){return v("cvm").discover(t)},request(t,e,r){return v("cvm").request(t,e,r)},listTools(t,e){return v("cvm").listTools(t,e)},callTool(t,e,r,i){return v("cvm").callTool(t,e,r,i)},listResources(t,e){return v("cvm").listResources(t,e)},readResource(t,e,r){return v("cvm").readResource(t,e,r)},close(t){return v("cvm").close(t)},onEvent(t){return v("cvm").onEvent(t)},registry:{list(t){return v("cvm").registry.list(t)},has(t,e){return v("cvm").registry.has(t,e)},describe(t,e){return v("cvm").registry.describe(t,e)},call(t,e,r,i){return v("cvm").registry.call(t,e,r,i)}}},je={getEvent(t,e){return v("outbox").getEvent(t,e)},query(t,e){return v("outbox").query(t,e)},subscribe(t,e){return v("outbox").subscribe(t,e)},publish(t,e){return v("outbox").publish(t,e)},resolveRelays(t){return v("outbox").resolveRelays(t)}},Z={open(t){return v("webrtc").open(t)},send(t,e){return v("webrtc").send(t,e)},close(t,e){return v("webrtc").close(t,e)},onEvent(t){return v("webrtc").onEvent(t)}};const ut=1,ht="naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqqxkutf4xfnrjefjxfnr2cm9gv6gmg",ft={pubkey:"60517f64548279854ee9a122531e4d1a413e3dc956e2d0a450217420623ca688",relays:["wss://relay.napplet.soy/"]},pt=[],yt=["worlds"],ne={version:ut,napplet:ht,provider:ft,boards:pt,modules:yt};class G extends Error{constructor(e,r,i){super(e),this.definitive=r,this.code=i}}function mt(t){async function e(r,i){var u,h,s,o;let n;const a=["soy_backend_invoke","soy_backend_changes","soy_backend_purge_plan","soy_backend_purge_confirm"].includes(r);try{n=await t.cvm.registry.call("soy.backends.v1",r,i,{timeoutMs:a?12e4:15e3})}catch(c){throw new G(c instanceof Error?c.message:"Connection interrupted",!1)}const l=n.structuredContent??JSON.parse(((h=(u=n.content)==null?void 0:u.find(c=>c.type==="text"))==null?void 0:h.text)??"{}");if(n.isError)throw new G(((s=l.error)==null?void 0:s.message)??"Backend rejected this operation",!0,(o=l.error)==null?void 0:o.code);return l}return{call:e,describe:(r,i)=>e("soy_backend_describe",{module:r,...i?{release:i}:{}}),intent:(r,i,n)=>({target:r,operation:i,input:n,requestId:crypto.randomUUID(),expiresAt:Math.floor(Date.now()/1e3)+240}),invoke:r=>e("soy_backend_invoke",r)}}function gt(t,e){return!!(t!=null&&t[e])}function he(t){return gt(window.napplet,t)}const oe=18,He=12,ae=[{name:"Air",color:"#ffffff"},{name:"Meadow",color:"#8ac987"},{name:"Pebble",color:"#b9a4cd"},{name:"Timber",color:"#dfb277"},{name:"Coral",color:"#f58d92"},{name:"Glacier",color:"#8bd5e5"},{name:"Butter",color:"#f2d58b"}],S=(t,e,r)=>t+r*oe+e*oe*oe;function ce(t,e,r){for(let i=He-1;i>=0;i--)if(t.blocks[S(e,i,r)])return i;return-1}function Ue(){const t=Array(oe*oe*He).fill(0),e=(r,i,n,a)=>t[S(r,i,n)]=a;for(let r=0;r<18;r++)for(let i=0;i<18;i++)e(r,0,i,2),e(r,1,i,2),e(r,2,i,r<2||i<2||r>15||i>15?6:1),r>12&&i<6&&e(r,3,i,1);for(const[r,i]of[[3,4],[13,12],[5,14]]){e(r,3,i,3),e(r,4,i,3);for(let n=-1;n<=1;n++)for(let a=-1;a<=1;a++)e(r+n,5,i+a,1);e(r,6,i,1)}for(let r=8;r<=11;r++)for(let i=3;i<=6;i++)e(r,3,i,3);for(const[r,i]of[[8,3],[11,3],[8,6],[11,6]])e(r,4,i,4),e(r,5,i,4);for(let r=8;r<=11;r++)for(let i=3;i<=6;i++)e(r,6,i,5);return{name:"Practice island",blocks:t,edits:0,locked:!1}}const pe=["#f07891","#75bfe0","#b799ed","#efba68","#85c8a7","#ee98ca"];function Se(t,e){let r=0;for(let i=0;i<e.length;i++){const n=e[i],a=e[(i+1)%e.length],l=(a.x-n.x)*(t.y-n.y)-(a.y-n.y)*(t.x-n.x);if(Math.abs(l)<.001)continue;const u=Math.sign(l);if(r&&u!==r)return!1;r=u}return!0}function vt(t,e,r,i){const n={x:r,y:i};for(let a=34;a>=0;a--)for(let l=17;l>=0;l--){const u=a-l;if(!(u<0||u>17))for(let h=ce(t,l,u);h>=0;h--){if(!t.blocks[S(l,h,u)])continue;const s=e(l,h+1,u),o=e(l+1,h+1,u),c=e(l,h+1,u+1),y=e(l+1,h+1,u+1),p=e(l+1,h,u+1);if(Se(n,[s,o,y,c])||Se(n,[o,y,p,e(l+1,h,u)])||Se(n,[c,y,p,e(l,h,u+1)]))return{x:l,z:u}}}}class wt{constructor(e,r){m(this,"canvas");m(this,"ctx");m(this,"world");m(this,"player",{x:8,z:11,color:0,name:"You"});m(this,"peers",new Map);m(this,"selected",{x:9,z:10});m(this,"active",!1);m(this,"pendingCells",new Set);m(this,"confirmedEdits",0);m(this,"keys",new Set);m(this,"zoom",1);m(this,"onSelect",()=>{});m(this,"w",900);m(this,"h",600);m(this,"unit",22);m(this,"clock",0);m(this,"motion",matchMedia("(prefers-reduced-motion: reduce)"));m(this,"last",0);m(this,"raf",0);m(this,"pan",{x:0,y:0});m(this,"particles",[]);this.canvas=e,this.ctx=e.getContext("2d"),this.world=r,this.raf=requestAnimationFrame(i=>this.frame(i))}project(e,r,i){return{x:this.w/2+(e-i)*this.unit+this.pan.x,y:this.h*.48+(e+i-17)*this.unit*.51-r*this.unit*.94+this.pan.y}}polygon(e,r,i){const n=this.ctx;n.beginPath(),n.moveTo(e[0].x,e[0].y);for(const a of e.slice(1))n.lineTo(a.x,a.y);n.closePath(),n.fillStyle=r,n.fill(),i&&(n.strokeStyle=i,n.lineWidth=.6,n.stroke())}shade(e,r){const i=parseInt(e.slice(1),16);return`rgb(${(i>>16)*r|0},${(i>>8&255)*r|0},${(i&255)*r|0})`}cube(e,r,i,n,a=1){const l=(c,y,p)=>this.project(c,y,p),u=l(e,r+a,i),h=l(e+a,r+a,i),s=l(e,r+a,i+a),o=l(e+a,r+a,i+a);this.polygon([s,o,l(e+a,r,i+a),l(e,r,i+a)],this.shade(n,.72)),this.polygon([h,o,l(e+a,r,i+a),l(e+a,r,i)],this.shade(n,.86)),this.polygon([u,h,o,s],n,"#44325c16")}ground(e,r){return ce(this.world,Math.max(0,Math.min(17,Math.floor(e))),Math.max(0,Math.min(17,Math.floor(r))))+1}pick(e,r){const i=vt(this.world,this.project.bind(this),e,r);return i&&(this.selected=i,this.onSelect()),!!i}burst(e,r,i,n){if(!this.motion.matches)for(let a=0;a<8;a++)this.particles.push({x:e+.5+(Math.random()-.5),z:i+.5+(Math.random()-.5),y:r+1+Math.random(),t:1,color:ae[n||2].color})}avatar(e){const r=this.ground(e.x,e.z),i=e.x-.24,n=e.z-.24,a=this.ctx,l=this.project(e.x,r+.01,e.z);a.fillStyle="#34254a35",a.beginPath(),a.ellipse(l.x,l.y,this.unit*.52,this.unit*.22,0,0,Math.PI*2),a.fill(),a.strokeStyle="#fffaf7",a.lineWidth=2,a.stroke(),this.cube(i,r,n,pe[e.color%6],.48),this.cube(i-.045,r+.49,n-.045,"#ffe4c9",.57),this.cube(i-.065,r+1.04,n-.065,pe[e.color%6],.61);const u=this.project(i+.59,r+.86,n+.34);a.fillStyle="#443253",a.fillRect(u.x-4,u.y,2.5,3.5),a.fillRect(u.x+2,u.y-3,2.5,3.5)}avatarLabel(e,r){const i=e===this.player?"YOU":e.name,n=this.project(e.x,this.ground(e.x,e.z)+1.8,e.z),a=this.ctx;a.font="700 11px ui-rounded, system-ui, sans-serif";const l=a.measureText(i).width+16,u={x:Math.max(l/2+8,Math.min(this.w-l/2-8,n.x)),y:n.y};for(;r.some(h=>Math.abs(h.x-u.x)<(h.width+l)/2+4&&Math.abs(h.y-u.y)<24);)u.y-=25;r.push({...u,width:l}),a.strokeStyle=pe[e.color],a.lineWidth=2,a.beginPath(),a.moveTo(n.x,n.y+9),a.lineTo(u.x,u.y+7),a.stroke(),a.fillStyle=i==="YOU"?"#584377":"#ffffff",a.beginPath(),a.roundRect(u.x-l/2,u.y-13,l,20,7),a.fill(),a.strokeStyle=pe[e.color],a.lineWidth=2,a.stroke(),a.fillStyle=i==="YOU"?"#ffffff":"#584377",a.textAlign="center",a.fillText(i,u.x,u.y+1)}frame(e){const r=Math.min((e-this.last)/1e3||0,.05);this.last=e,this.clock=e;const i=this.ctx;this.w=this.canvas.clientWidth,this.h=this.canvas.clientHeight;const n=Math.min(devicePixelRatio,2);if((this.canvas.width!==Math.round(this.w*n)||this.canvas.height!==Math.round(this.h*n))&&(this.canvas.width=Math.round(this.w*n),this.canvas.height=Math.round(this.h*n)),i.setTransform(n,0,0,n,0,0),this.unit=Math.max(10,Math.min(this.w<600?this.w/26:this.w/43,this.h/29,33))*this.zoom,this.active){let s=0,o=0;if((this.keys.has("KeyW")||this.keys.has("ArrowUp"))&&o--,(this.keys.has("KeyS")||this.keys.has("ArrowDown"))&&o++,(this.keys.has("KeyA")||this.keys.has("ArrowLeft"))&&s--,(this.keys.has("KeyD")||this.keys.has("ArrowRight"))&&s++,s||o){const c=r*3/Math.hypot(s,o);this.player.x=Math.max(.5,Math.min(17.45,this.player.x+(s+o)*c)),this.player.z=Math.max(.5,Math.min(17.45,this.player.z+(-s+o)*c)),this.selected={x:Math.max(0,Math.min(17,Math.floor(this.player.x+(s+o)*.8))),z:Math.max(0,Math.min(17,Math.floor(this.player.z+(-s+o)*.8)))},this.onSelect()}}for(const[s,o]of this.peers){if(e-o.seen>1e4){this.peers.delete(s);continue}o.x+=(o.tx-o.x)*Math.min(1,r*12),o.z+=(o.tz-o.z)*Math.min(1,r*12)}const a=i.createLinearGradient(0,0,0,this.h);a.addColorStop(0,"#dbd9f5"),a.addColorStop(1,"#f5e4e5"),i.fillStyle=a,i.fillRect(0,0,this.w,this.h),i.fillStyle="#ffffff70";for(let s=0;s<5;s++){const o=(s*233+(this.motion.matches?0:e*.004))%(this.w+190)-90,c=85+s%3*75;i.beginPath(),i.roundRect(o,c,100,15,8),i.roundRect(o+24,c-12,42,24,10),i.fill()}const l=this.project(9,-1.8,9);i.fillStyle="#9881b025",i.beginPath(),i.ellipse(l.x,l.y,this.unit*16,this.unit*6,0,0,Math.PI*2),i.fill();const u=[...this.active?[this.player]:[],...this.peers.values()];for(let s=0;s<35;s++)for(let o=0;o<18;o++){const c=s-o;if(c<0||c>17)continue;const y=ce(this.world,o,c);for(let p=0;p<=y;p++){const w=this.world.blocks[S(o,p,c)];w&&(o<17&&c<17&&this.world.blocks[S(o+1,p,c)]&&this.world.blocks[S(o,p,c+1)]&&this.world.blocks[S(o,p+1,c)]||this.cube(o,p,c,ae[w].color))}if(y===2&&this.world.blocks[S(o,2,c)]===1&&(o*13+c*7)%31===0){const p=this.project(o+.5,3.03,c+.5);i.fillStyle="#fff4ce",i.fillRect(p.x-2,p.y-4,4,4),i.fillStyle="#619273",i.fillRect(p.x,p.y,1.5,4)}if(this.pendingCells.has(o+c*18)){const p=y+1.04;i.setLineDash([4,3]),this.polygon([this.project(o,p,c),this.project(o+1,p,c),this.project(o+1,p,c+1),this.project(o,p,c+1)],"#fff6ce40","#fff2a6"),i.setLineDash([])}if(this.active&&o===this.selected.x&&c===this.selected.z){const p=y+1.025,w=[this.project(o,p,c),this.project(o+1,p,c),this.project(o+1,p,c+1),this.project(o,p,c+1)];this.polygon(w,"#ffffff35","#ffffff"),i.lineWidth=2,i.stroke()}}u.sort((s,o)=>s.x+s.z-o.x-o.z);for(const s of u)this.avatar(s);const h=[];for(const s of u)this.avatarLabel(s,h);for(const s of this.particles){s.t-=r*2,s.y+=r*2;const o=this.project(s.x,s.y,s.z);i.globalAlpha=Math.max(0,s.t),i.fillStyle=s.color,i.fillRect(o.x,o.y,5,5)}i.globalAlpha=1,this.particles=this.particles.filter(s=>s.t>0),this.canvas.dataset.localX=this.player.x.toFixed(3),this.canvas.dataset.localZ=this.player.z.toFixed(3),this.canvas.dataset.peerCount=String(this.peers.size),this.canvas.dataset.peerLabels=JSON.stringify([...this.peers.values()].map(s=>s.name)),this.canvas.dataset.peerPositions=JSON.stringify([...this.peers].map(([s,o])=>[s,Number(o.x.toFixed(3)),Number(o.z.toFixed(3))])),this.canvas.dataset.edits=String(this.world.edits),this.canvas.dataset.confirmedEdits=String(this.confirmedEdits),this.canvas.dataset.pendingCells=String(this.pendingCells.size),this.canvas.dataset.selected=`${this.selected.x},${this.selected.z}`,this.raf=requestAnimationFrame(s=>this.frame(s))}dispose(){cancelAnimationFrame(this.raf)}}const ie=t=>({...t,blocks:[...t.blocks]});function Ee(t,e){return[e.x,e.y,e.z,e.expected,e.block].every(Number.isInteger)&&e.x>=0&&e.x<18&&e.z>=0&&e.z<18&&e.y>=2&&e.y<12&&e.block>=0&&e.block<=6&&e.expected>=0&&e.expected<=6&&!t.locked&&t.blocks[S(e.x,e.y,e.z)]===e.expected&&e.block!==e.expected&&(e.block===0?ce(t,e.x,e.z)===e.y:e.expected===0&&t.blocks[S(e.x,e.y-1,e.z)]!==0)}function Je(t,e){t.blocks[S(e.x,e.y,e.z)]=e.block,t.edits++}function ze(t,e){const r=t;if(!r||JSON.stringify(r.target)!==JSON.stringify(e)||!Array.isArray(r.queue)||r.queue.length>24||!r.queue.every(l=>l&&[l.x,l.y,l.z,l.expected,l.block].every(Number.isInteger)&&l.x>=0&&l.x<18&&l.z>=0&&l.z<18&&l.y>=2&&l.y<12&&l.expected>=0&&l.expected<=6&&l.block>=0&&l.block<=6))return!1;if(!r.active)return!0;const{intent:i,count:n}=r.active;if(!i||JSON.stringify(i.target)!==JSON.stringify(e)||typeof i.requestId!="string"||!Number.isSafeInteger(i.expiresAt)||!Number.isInteger(n)||n<1||n>8||n>r.queue.length)return!1;const a=i.input;return i.operation==="editBlock"?n===1&&JSON.stringify(a)===JSON.stringify(r.queue[0]):i.operation==="editBlocks"&&Number.isSafeInteger(a.after)&&Number(a.after)>=0&&JSON.stringify(a.edits)===JSON.stringify(r.queue.slice(0,n))}const ke=class ke{constructor(e,r,i,n,a,l,u,h){m(this,"world");m(this,"revision");m(this,"queue",[]);m(this,"active");m(this,"running",!1);m(this,"stopped",!1);m(this,"retryTimer");m(this,"retryDelay",1e3);m(this,"conflicts",0);m(this,"waiting",!1);m(this,"resuming",!1);m(this,"recovery",!1);m(this,"refreshRequested",!1);m(this,"force",!1);m(this,"lastHint",-1/0);m(this,"hintTimer");m(this,"message","");m(this,"saveChain",Promise.resolve());this.target=e,this.compact=n,this.api=a,this.effects=l,this.actor=h,this.world=ie(r),this.revision=i,u&&ze(u,e)&&(u.actor!==h?(this.recovery=!0,this.effects.notice("Your player session changed. Pending work was cancelled; review the saved world.")):(this.queue=structuredClone(u.queue),this.active=structuredClone(u.active),this.resuming=!!this.active))}get hasWork(){return this.queue.length>0||!!this.active||this.recovery}get uncertain(){return this.waiting}start(){this.render(),this.pump()}dispose(){this.stopped=!0,clearTimeout(this.retryTimer),clearTimeout(this.hintTimer)}journal(){return this.queue.length?{actor:this.actor,target:this.target,queue:structuredClone(this.queue),...this.active?{active:structuredClone(this.active)}:{}}:null}persist(){const e=this.journal();return this.saveChain=this.saveChain.catch(()=>{}).then(()=>this.stopped?void 0:this.effects.persist(e)),this.saveChain}predicted(){const e=ie(this.world);if(!this.resuming)for(const r of this.queue)Ee(e,r)&&Je(e,r);return e}render(){this.stopped||this.effects.change({world:this.predicted(),revision:this.revision,confirmedEdits:this.world.edits,pending:this.queue.length,cells:new Set(this.queue.map(e=>e.x+e.z*18)),state:this.waiting?"retry":this.recovery||this.resuming?"refreshing":this.queue.length?"saving":"saved",message:this.message})}enqueue(e){return this.stopped||this.recovery||this.resuming?(this.effects.notice("Refreshing saved blocks. Try again in a moment."),!1):this.queue.length>=ke.capacity?(this.effects.notice("Save queue full (24). Wait for a save or retry the connection."),!1):Ee(this.predicted(),e)?(this.queue.length||(this.conflicts=0),this.queue.push({...e}),this.render(),this.persist().catch(()=>{this.message="Could not keep recovery data. Retry save.",this.render()}),this.pump(),!0):(this.effects.notice("That block changed. Aim again before building."),!1)}retry(){clearTimeout(this.retryTimer),this.waiting=!1,this.pump()}sync(e=!1){if(!this.stopped){if(e){const r=500-(Date.now()-this.lastHint);if(r>0){this.hintTimer||(this.hintTimer=setTimeout(()=>{this.hintTimer=void 0,this.sync(!0)},r+1));return}clearTimeout(this.hintTimer),this.hintTimer=void 0,this.lastHint=Date.now(),this.force=!0}this.refreshRequested=!0,this.pump()}}acceptSnapshot(e,r){var i;if(!(r<this.revision)){if(!e||((i=e.blocks)==null?void 0:i.length)!==3888)throw new Error("Invalid saved world.");this.world=ie(e),this.revision=r}}acceptDelta(e,r){if(r<this.revision)return!0;if(e.reset||e.from>this.world.edits||e.edits<this.world.edits)return!1;const i=ie(this.world);for(const n of e.changes)if(!(n.n<=i.edits)){if(n.n!==i.edits+1)return!1;i.blocks[S(n.x,n.y,n.z)]=n.block,i.edits=n.n}return i.edits!==e.edits?!1:(i.locked=e.locked,this.world=i,this.revision=r,!0)}async read(e=!1){const r=this.compact&&!e?"readUpdates":"readWorld",i=await this.api.invoke(this.api.intent(this.target,r,r==="readWorld"?{}:{after:this.world.edits}));this.stopped||(r==="readWorld"?this.acceptSnapshot(i.result,i.revision):this.acceptDelta(i.result,i.revision)||await this.read(!0))}reconcileQueue(){const e=ie(this.world);for(const r of this.queue){if(!Ee(e,r)){this.queue=[],this.effects.notice("Saved blocks changed. Unsent edits were cancelled; aim again.");break}Je(e,r)}}backoff(e){this.stopped||(this.waiting=!0,this.message=e,clearTimeout(this.retryTimer),this.retryTimer=setTimeout(()=>this.retry(),this.retryDelay),this.retryDelay=Math.min(8e3,this.retryDelay*2))}async pump(){if(!(this.running||this.stopped||this.waiting)){this.running=!0;try{for(;!this.stopped;){if(this.recovery)try{if(await this.read(!0),this.stopped)return;this.recovery=!1,this.resuming=!1,this.message="",this.reconcileQueue(),await this.persist(),this.render()}catch{this.backoff("Could not refresh saved blocks. Retrying…");break}if(this.queue.length){if(!this.active){if(this.reconcileQueue(),!this.queue.length){await this.persist(),this.render();continue}const e=this.compact?Math.min(8,this.queue.length):1,r=this.compact?{after:this.world.edits,edits:this.queue.slice(0,e).map(i=>({...i}))}:{...this.queue[0]};this.active={intent:this.api.intent(this.target,this.compact?"editBlocks":"editBlock",r),count:e}}if(this.active.intent.expiresAt<=Date.now()/1e3){this.queue=[],this.active=void 0,this.recovery=!0,this.effects.notice("Save retry expired. Refreshing; review your blocks before editing again.");continue}try{if(await this.persist(),this.stopped)return;const e=this.active,r=await this.api.invoke(e.intent);if(this.stopped)return;this.queue.splice(0,e.count),this.active=void 0,this.resuming=!1,e.intent.operation==="editBlock"?this.acceptSnapshot(r.result,r.revision):this.acceptDelta(r.result,r.revision)||(this.recovery=!0),this.reconcileQueue(),await this.persist(),this.retryDelay=1e3,this.conflicts=0,this.message="",this.effects.saved(r.revision),this.render()}catch(e){if(this.stopped)return;if(e instanceof G&&e.definitive){this.active=void 0,this.resuming=!1,this.recovery=!0,e.code==="CONFLICT"&&++this.conflicts<=2?this.message="Checking changes from friends…":(this.queue=[],this.effects.notice(e.code==="CONFLICT"?"World is busy. Pending edits cancelled; try again after it refreshes.":e.message)),await this.persist(),this.render();continue}this.backoff(this.active?"Save unconfirmed · Retrying safely…":"Recovery data needs a retry.");break}continue}if(this.refreshRequested){const e=this.force;this.force=!1,this.refreshRequested=!1;try{if(e)await this.read();else{const r=await this.api.call("soy_backend_changes",{target:this.target,after:this.revision});if(this.stopped)return;r.revision>this.revision&&await this.read()}if(this.stopped)return;this.reconcileQueue(),await this.persist(),this.message="",this.render()}catch(r){this.refreshRequested=!0,r instanceof G&&r.code==="RESYNC_REQUIRED"&&(this.recovery=!0),this.backoff("Connection interrupted · Saved blocks will refresh.");break}continue}break}}catch{this.backoff("Recovery data needs a retry.")}finally{this.running=!1,this.render()}}}};m(ke,"capacity",24);let Ie=ke;const Y="soy.app-data/1",K=30078,Oe=16*1024,Xe=/^[a-f0-9]{64}$/;function ye(t,e,r){if(typeof t!="string"||typeof e!="string"||typeof r!="string"||!Xe.test(t)||!/^[a-z][a-z0-9_-]{0,47}$/.test(e)||!/^[a-zA-Z0-9_-]{1,64}$/.test(r))throw new Error("app-data-invalid-id: use a collection name and a stable record ID.");return`${Y}:${t}:${e}:${r}`}function Ze(t){let e=0;const r=new Set,i=(n,a)=>{if(++e>4096||a>12)throw new Error("app-data-too-complex: simplify the data or use Blossom.");if(!(n===null||typeof n=="boolean")&&!(typeof n=="number"&&Number.isFinite(n))&&!(typeof n=="string"&&n.length<=Oe)){if(typeof n!="object"||n===null||r.has(n))throw new Error("app-data-invalid-json: data must contain only finite JSON values.");if(r.add(n),Array.isArray(n)){if(n.length>1024)throw new Error("app-data-too-complex: arrays are limited to 1024 items.");for(const l of n)i(l,a+1)}else{if(Object.getPrototypeOf(n)!==Object.prototype&&Object.getPrototypeOf(n)!==null)throw new Error("app-data-invalid-json: use plain JSON objects.");const l=Object.keys(n);if(l.length>128||l.some(u=>u.length>128||["__proto__","constructor","prototype"].includes(u)))throw new Error("app-data-invalid-json: invalid object keys or too many fields.");for(const u of l)i(n[u],a+1)}r.delete(n)}};i(t,0)}function Re(t,e,r,i,n){return[["d",ye(t,e,r)],["s",t],["c",e],["L",Y],["l",i,Y],["v",String(n)]]}function Ae(t){var s;if(t.kind!==K||typeof t.content!="string"||new TextEncoder().encode(t.content).length>Oe)throw new Error("app-data-invalid-record: expected a kind-30078 record of at most 16 KiB.");let e;try{e=JSON.parse(t.content)}catch{throw new Error("app-data-invalid-json: record content must be JSON.")}if(Ze(e),!e||Array.isArray(e)||typeof e!="object"||Object.keys(e).sort().join(",")!=="data,deleted,previous,schema,title,version"||typeof e.schema!="string"||!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,95}$/.test(e.schema)||!Number.isSafeInteger(e.version)||e.version<1||e.version>1e6||typeof e.title!="string"||!e.title.trim()||e.title.length>160||/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/.test(e.title)||e.previous!==null&&(typeof e.previous!="string"||!Xe.test(e.previous))||typeof e.deleted!="boolean"||e.deleted&&(e.data!==null||e.previous===null))throw new Error("app-data-invalid-record: check schema, version, title, previous revision and deletion fields.");if(!Array.isArray(t.tags)||t.tags.length!==6)throw new Error("app-data-invalid-tags: use the documented application-data tags.");const r=((s=t.tags.find(o=>o[0]==="d"))==null?void 0:s[1])??"",i=r.split(":"),[n,a,l,u]=i;if(i.length!==4||n!==Y)throw new Error("app-data-invalid-id: unsupported record identifier.");if(!Re(a,l,u,e.schema,e.version).every(o=>t.tags.filter(c=>JSON.stringify(c)===JSON.stringify(o)).length===1))throw new Error("app-data-invalid-tags: record tags must match its schema and namespace.");return{scope:a,collection:l,id:u,identifier:r,body:e}}async function bt(t){var u;const e=t.host??globalThis.napplet;if(!((u=e==null?void 0:e.shell)!=null&&u.onReady)||!e.outbox||!e.identity)throw new Error("app-data-unavailable: this host needs shell, outbox and identity support.");const r=await new Promise((h,s)=>{let o,c=!1;const y=setTimeout(()=>{o==null||o.close(),s(new Error("app-data-unavailable: host initialization did not finish. Keep drafts locally and retry."))},5e3);o=e.shell.onReady(p=>{var w;c=!0,clearTimeout(y),o==null||o.close(),h((w=p.capabilities)==null?void 0:w.appData)}),c&&o.close()});if((r==null?void 0:r.profile)!==Y)throw new Error("app-data-unavailable: update soyLI or use a host supporting soy.app-data/1. Keep drafts locally.");const i=t.relays??r.defaultRelays;if(!Array.isArray(i)||!i.length||i.length>8)throw new Error("app-data-no-relays: choose a storage relay in host Network settings.");ye(r.scope,t.collection,"check"),Ae({kind:K,tags:Re(r.scope,t.collection,"check",t.schema,t.version),content:JSON.stringify({schema:t.schema,version:t.version,title:"Check",previous:null,deleted:!1,data:null})});const n=h=>{const{scope:s,collection:o,id:c,body:y}=Ae(h);if(y.schema!==t.schema||y.version!==t.version||o!==t.collection)throw new Error("app-data-incompatible: this record uses another collection or schema version.");return{id:c,author:h.pubkey,scope:s,collection:o,title:y.title,revision:h.id,createdAt:h.created_at,deleted:y.deleted,data:y.deleted?null:t.validate(y.data),event:h}},a=async h=>{const s=await e.outbox.query(h,{relays:i,timeoutMs:8e3});if(s.error)throw new Error(s.error);return s},l=h=>h.sort((s,o)=>o.created_at-s.created_at||s.id.localeCompare(o.id));return{policy:r,relays:i,async list(h={}){var X;const s=h.scope??r.scope;ye(s,t.collection,"check");const o=h.limit??40;if(!Number.isSafeInteger(o)||o<1||o>100||h.until!==void 0&&(!Number.isSafeInteger(h.until)||h.until<0))throw new Error("app-data-invalid-query: limit must be 1–100 and until a Unix timestamp.");const c=await a([{kinds:[K],"#s":[s],"#c":[t.collection],"#L":[Y],limit:o,...h.until===void 0?{}:{until:h.until}}]),y=new Map;for(const{event:T}of c.events){const N=(X=T.tags.find(W=>W[0]==="d"))==null?void 0:X[1];if(T.kind!==K||!(N!=null&&N.startsWith(`${Y}:${s}:${t.collection}:`)))continue;const te=`${T.pubkey}:${N}`,re=y.get(te);(!re||l([re,T])[0]===T)&&y.set(te,T)}const p=l([...y.values()]).slice(0,o),w=[];let b=0;for(const T of p)try{const N=n(T);N.deleted||w.push(N)}catch{b++}return{records:w,incomplete:!!c.incomplete,invalid:b,nextUntil:p.length>=o?p.at(-1).created_at:null}},async get(h,s,o=r.scope){if(!/^[a-f0-9]{64}$/.test(h))throw new Error("app-data-invalid-author: expected a public key.");const c=ye(o,t.collection,s),y=await a([{kinds:[K],authors:[h],"#d":[c],limit:1}]),p=l(y.events.map(w=>w.event).filter(w=>w.kind===K&&w.pubkey===h&&w.tags.some(b=>b[0]==="d"&&b[1]===c)))[0];return{record:p?n(p):null,incomplete:!!y.incomplete}},async prepare(h){const s=await e.identity.getPublicKey();if(!/^[a-f0-9]{64}$/.test(s))throw new Error("not-signed-in: connect a viewer identity to publish.");const{base:o}=h;if(o&&(o.author!==s||o.scope!==r.scope||o.collection!==t.collection||o.id!==h.id))throw new Error("app-data-owner-mismatch: copy this creation to a new ID to make it your own.");const c={schema:t.schema,version:t.version,title:h.title,previous:(o==null?void 0:o.revision)??null,deleted:h.deleted??!1,data:null};if(!c.deleted){const p=t.validate(h.data);Ze(p),c.data=p}const y={kind:K,created_at:Math.max(Math.floor(Date.now()/1e3),((o==null?void 0:o.createdAt)??0)+1),tags:Re(r.scope,t.collection,h.id,t.schema,t.version),content:JSON.stringify(c)};if(Ae(y),new TextEncoder().encode(y.content).length>Math.min(r.maxContentBytes,Oe))throw new Error("app-data-too-large: store the large payload on Blossom and publish its URL/hash.");return{template:structuredClone(y),async publish(){if(await e.identity.getPublicKey()!==s)throw new Error("app-data-identity-changed: prepare the change again with the selected identity.");const p=await e.outbox.publish(structuredClone(y),{relays:i,toOutbox:!1});if(!p.ok||!p.event)throw new Error(p.error??"app-data-publish-failed: no signed record returned.");if(p.event.pubkey!==s||p.event.kind!==y.kind||p.event.created_at!==y.created_at||p.event.content!==y.content||JSON.stringify(p.event.tags)!==JSON.stringify(y.tags))throw new Error("app-data-invalid-result: host returned a different record.");return n(p.event)}}}}}function Ge(t){if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).join(",")!=="code")throw new Error("Invalid world listing.");const e=t.code;if(typeof e!="string"||e.length>300||!/^NC1-[A-Za-z0-9_-]+$/.test(e))throw new Error("Invalid world code in listing.");let r;try{r=JSON.parse(atob(e.slice(4).replaceAll("-","+").replaceAll("_","/")))}catch{throw new Error("Invalid world code in listing.")}if(!Array.isArray(r)||r.length!==2||typeof r[0]!="string"||!/^[a-f0-9]{64}$/.test(r[0])||typeof r[1]!="string"||!/^[A-Za-z0-9_-]{1,64}$/.test(r[1]))throw new Error("Invalid world code in listing.");return{code:"NC1-"+btoa(JSON.stringify(r)).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"")}}async function kt(t){const e=new TextEncoder().encode(Ge({code:t}).code),r=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(r)].map(i=>i.toString(16).padStart(2,"0")).join("")}function xt(t,e=""){const r=new Map;for(const i of t){if(!i.data||i.deleted)continue;const n=r.get(i.data.code);(!n||i.author===e&&n.author!==e||n.author!==e&&(i.createdAt>n.createdAt||i.createdAt===n.createdAt&&i.revision<n.revision))&&r.set(i.data.code,i)}return[...r.values()].sort((i,n)=>n.createdAt-i.createdAt||i.revision.localeCompare(n.revision))}async function Ct(){var e;const t=globalThis.napplet;if(!((e=t==null?void 0:t.shell)!=null&&e.onReady))throw new Error("This host does not offer the public world list. You can still join with a code.");return bt({collection:"worlds",schema:"napcraft.world-directory",version:1,validate:Ge,host:{shell:t.shell,identity:se,outbox:{query:(r,i)=>je.query(r,i),publish:(r,i)=>je.publish(r,i)}}})}let _e;function Qe(){return _e??(_e=Ct().catch(t=>{throw _e=void 0,t}))}function Ne(t){const e=t instanceof Error?t.message:String(t);return/app-data-unavailable|app-data-no-relays/.test(e)?"Public worlds are unavailable on this host. You can still create, practice or join with a code.":e}function St(t,e,r){t.innerHTML='<div class="directory-heading"><h2>Public worlds</h2><button class="directory-refresh" type="button">↻ Refresh</button></div><p class="directory-caption">Pick an island and make yourself at home. Listed worlds stay here when everyone leaves.</p><label class="sr-only" for="worldSearch">Search loaded worlds</label><input id="worldSearch" type="search" placeholder="Find an island…" maxlength="80"/><p class="directory-status" role="status" aria-live="polite"></p><div class="world-list"></div>';const i=t.querySelector("button"),n=t.querySelector("input"),a=t.querySelector(".directory-status"),l=t.querySelector(".world-list");let u=[],h=!1,s="",o=0,c=!1;const y=()=>{l.replaceChildren();const w=u.filter(b=>b.title.toLocaleLowerCase().includes(n.value.trim().toLocaleLowerCase()));for(const b of w){const X=document.createElement("article");X.className="world-row";const T=document.createElement("span");T.className="world-emblem",T.textContent="▧",T.setAttribute("aria-hidden","true");const N=document.createElement("div");N.className="world-copy";const te=document.createElement("strong");te.textContent=b.title;const re=document.createElement("span");re.textContent=b.author===r?"Shared by you":`Shared by ${b.author.slice(0,8)}…`,N.append(te,re);const W=document.createElement("button");W.className="world-join primary",W.textContent="Join",W.setAttribute("aria-label",`Join ${b.title}`),W.disabled=c,W.onclick=async()=>{if(!c){c=!0,W.textContent="Joining…",t.querySelectorAll(".world-join").forEach(at=>at.disabled=!0);try{await e(b.data.code)}finally{c=!1,t.isConnected&&y()}}},X.append(T,N,W),l.append(X)}a.textContent=s||(h?u.length?w.length?`${w.length} recently listed ${w.length===1?"world":"worlds"} · anyone can join and build`:"No matching islands in the loaded list.":"No public worlds yet. Create one, then choose Invite → List publicly.":"Looking for islands…")},p=async()=>{const w=++o;i.disabled=!0,s="Looking for islands…",y();try{const b=await(await Qe()).list({limit:40});if(!t.isConnected||w!==o)return;u=xt(b.records,r),h=!0,s=[b.incomplete?"Some listings could not be checked. Refresh to try again.":"",b.invalid?`${b.invalid} invalid or unsupported listings hidden.`:"",b.nextUntil?"Showing a recent selection, not every public world.":""].filter(Boolean).join(" ")}catch(b){if(!t.isConnected||w!==o)return;s=`${Ne(b)}${u.length?" Showing earlier results.":""}`}finally{t.isConnected&&w===o&&(i.disabled=!1,y())}};n.oninput=y,i.onclick=()=>void p(),p()}function Et(t,e,r){t.innerHTML='<h3>Make room for more builders</h3><p>List this island on Napcraft’s opening screen. Anyone can join and edit, even after you leave. Only list a world you want to share widely.</p><button id="listWorld" class="wide" disabled>Checking your listing…</button><p class="listing-status footnote" role="status" aria-live="polite"></p>';const i=t.querySelector("button"),n=t.querySelector(".listing-status"),a=async()=>{let l;i.disabled=!0;try{const u=await se.getPublicKey();if(!t.isConnected)return;if(!u){i.textContent="Sign in to list publicly",n.textContent="Connect your account in the host. Browsing and joining need no sign-in.";return}const h=await Qe(),s=await kt(e),o=await h.get(u,s);if(!t.isConnected)return;if(o.incomplete)throw new Error("Could not fully check your listing. Retry before changing it.");const c=!!(o.record&&!o.record.deleted);i.textContent=c?"Remove my listing":"List publicly",i.disabled=!1,n.textContent=c?"Your listing stays available when the world is empty. Removing it keeps the world and its code intact. Others may have shared their own listing.":"Listing is a separate public announcement. Your world is already saved.",i.onclick=async()=>{if(!i.disabled){i.disabled=!0,i.textContent="Waiting for approval…",n.textContent="Approve the public listing change in your host.";try{if(l??(l=await h.prepare({id:s,title:r.replace(/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/g,"").trim()||"Shared island",base:o.record,data:{code:e},deleted:c})),await l.publish(),!t.isConnected)return;l=void 0,await a()}catch(y){t.isConnected&&(n.textContent=`${Ne(y)} Your world is still saved. Retry this listing change or reopen Invite to check its current status.`,i.textContent=c?"Retry removing listing":"Retry listing",i.disabled=!1)}}}}catch(u){t.isConnected&&(n.textContent=Ne(u),i.textContent="Retry checking listing",i.disabled=!1,i.onclick=()=>void a())}};a()}const d=t=>document.getElementById(t),f=new wt(d("scene"),Ue()),J=mt({cvm:Fe}),le={napplet:ne.napplet,name:"worlds"};async function xe(t){for(let e=0;;e++)try{return await J.invoke(J.intent(t,"readWorld",{}))}catch(r){if(!(r instanceof G)||r.code!=="CONFLICT"||e>=2)throw r;await new Promise(i=>setTimeout(i,150*(e+1)))}}let q=[],k,$=[],x,R="lobby",Me=4,I=!1,P="",At=0,Te=!1,Le=-1,F="Waiting for friends",Ve="",Q=!1,et=0,tt=0,Be=0,me,ge="",M=0;const Tt="napcraft-presence-v1",D=new Set;let g,H;async function qt(t){H=t??void 0,he("storage")&&await L.setItem("napcraft-edit-journal-v1",JSON.stringify(t))}function V(){return!!(x||g!=null&&g.hasWork||H)}async function It(){if(g){g.retry(),fe();return}if(!(I||!H)){A(!0);try{const t=H,e=We(be(t.target));if(!ze(t,e))throw new Error("Invalid recovery data. Keep your world code and contact the creator.");const r=await xe(e);await de(e,r.result,r.revision,t)}catch(t){z(t)}finally{A(!1)}}}async function rt(){if(!(!P||!D.size||!k))try{await Z.send(P,{v:1,kind:"saved",instance:k.instance,release:k.release})}catch{}}f.player.color=crypto.getRandomValues(new Uint8Array(1))[0]%6;function E(t){d("toast").textContent=t,d("toast").hidden=!1,clearTimeout(Be),Be=window.setTimeout(()=>d("toast").hidden=!0,4200)}function ee(t,e=!1){d("saveStatus").textContent=t,d("saveStatus").classList.toggle("warning",e)}function _(t){f.keys.clear(),d("panel").classList.remove("directory-panel"),d("overlay").hidden=!1,d("panel").innerHTML=t;const e=d("close");e&&(e.onclick=fe),setTimeout(()=>{var r;return(r=d("panel").querySelector("input,button"))==null?void 0:r.focus()},20)}function fe(){R!=="lobby"&&(d("overlay").hidden=!0,f.canvas.focus())}const B=()=>R==="lobby"?"":'<button id="close" class="close" aria-label="Close dialog">×</button>';function ve(t){const e=document.createElement("span");return e.textContent=t,e.innerHTML}function be(t){return"NC1-"+btoa(JSON.stringify([t.release,t.instance])).replaceAll("+","-").replaceAll("/","_").replace(/=+$/,"")}function We(t){try{if(t.length>300||!t.startsWith("NC1-"))throw 0;const[e,r]=JSON.parse(atob(t.trim().slice(4).replaceAll("-","+").replaceAll("_","/")));if(!/^[a-f0-9]{64}$/.test(e)||typeof r!="string"||!/^[a-zA-Z0-9_-]{1,64}$/.test(r))throw 0;return{module:le,release:e,instance:r}}catch{throw new Error("That world code looks incomplete. Paste the entire NC1- code.")}}async function j(t=!1){if(!he("storage")){R==="shared"&&E("Keep your world code to return. This host has no recent-world storage.");return}try{await L.setItem("napcraft-worlds-v1",JSON.stringify($)),await L.setItem("napcraft-pending-v1",JSON.stringify(x??null))}catch{if(t)throw new Error("Could not keep your recovery request. Retry when host storage is available.");E("World is online; recent-world saving failed. Keep the world code.")}}async function Ke(){var t,e,r,i;if($=[],x=void 0,H=void 0,q=[],he("storage"))try{const n=await L.getItem("napcraft-worlds-v1"),a=n?JSON.parse(String(n)):[];$=Array.isArray(a)?a.filter(s=>typeof(s==null?void 0:s.name)=="string"&&typeof(s==null?void 0:s.code)=="string").slice(0,8):[];const l=await L.getItem("napcraft-edit-journal-v1");if(l){const s=JSON.parse(String(l));((e=(t=s==null?void 0:s.target)==null?void 0:t.module)==null?void 0:e.napplet)===ne.napplet&&s.target.module.name==="worlds"&&ze(s,s.target)&&(H=s)}const u=await L.getItem("napcraft-pending-v1");if(u){const s=JSON.parse(String(u));((i=(r=s==null?void 0:s.target)==null?void 0:r.module)==null?void 0:i.napplet)===ne.napplet&&typeof s.requestId=="string"&&(x=s)}const h=JSON.parse(String(await L.getItem("napcraft-kept-saves-v1")||"[]"));Array.isArray(h)&&(q=h.filter(s=>{var o,c,y,p,w,b;return s&&typeof s.id=="string"&&(((y=(c=(o=s.intent)==null?void 0:o.target)==null?void 0:c.module)==null?void 0:y.napplet)===ne.napplet||((b=(w=(p=s.journal)==null?void 0:p.target)==null?void 0:w.module)==null?void 0:b.napplet)===ne.napplet)}).slice(0,8))}catch{}}function O(){_(`${B()}<span class="stamp">WELCOME TO NAPCRAFT</span><div class="lobby-heading"><h1>Find your little<br>corner of the world.</h1><button id="create" class="primary">✦ Create a world</button></div>${V()||g!=null&&g.uncertain?'<button id="recover" class="wide">Review unfinished save</button>':""}${q.length?'<button id="kept" class="wide">Kept recovery records</button>':""}<section id="publicWorlds" aria-label="Public worlds"></section><p id="error" class="error" role="status"></p><div class="secondary-row"><button id="join">↗ Join with a code</button><button id="practice">Try a practice island</button></div>${$.length?'<h3 class="recents-heading">Your recent worlds</h3>':""}<div id="recents"></div><p class="footnote">Sign in to create or list a world. Friends can join as guests.<br>Listings are shared invitations, not live player counts.</p>`),d("panel").classList.add("directory-panel"),St(d("publicWorlds"),t=>$e(t),ge),d("create").onclick=()=>it(),d("join").onclick=Rt,d("practice").onclick=()=>void zt(),d("recover")&&(d("recover").onclick=()=>U()),d("kept")&&(d("kept").onclick=Nt);for(const t of $){const e=document.createElement("button");e.className="recent",e.textContent=`▧ ${t.name}  →`,d("recents").append(e),e.onclick=()=>void $e(t.code)}}function it(t=!1){if(V()){U();return}const e=q.find(r=>{var i;return((i=r.intent)==null?void 0:i.operation)==="createWorld"&&!r.code});if(e&&!t){U(e);return}if(_(`${B()}<span class="stamp">A FRESH START</span><h2>Your world awaits.</h2><label for="name">Give your island a name</label><input id="name" maxlength="48" value="Our little island"/><button id="confirmCreate" class="primary wide">Create shared world</button><p class="footnote">Your host will ask to use your signed-in account. Anyone with the code can build here. Worlds are public; the code is not a password.</p><p id="error" class="error"></p><button id="back" class="linkish">← Back</button>`),e){const r=document.createElement("label");r.className="recovery-consent",r.innerHTML='<input id="separateWorld" type="checkbox"/> I want a separate world. My older request may already have created one; keep its recovery record.',d("confirmCreate").before(r),d("name").value="A different little island"}d("confirmCreate").onclick=()=>void Lt(),d("back").onclick=O}function Rt(){_(`${B()}<span class="stamp">BETTER TOGETHER</span><h2>Meet on the island.</h2><label for="code">Paste a world code</label><input id="code" class="code" placeholder="NC1-…" autocomplete="off" spellcheck="false"/><button id="confirmJoin" class="primary wide">Join world</button><p id="error" class="error"></p><p class="footnote">Keep this code: it works again after everyone leaves. Anyone with it can build.</p><button id="back" class="linkish">← Back</button>`),d("confirmJoin").onclick=()=>void $e(d("code").value.trim()),d("back").onclick=O}function z(t){const e=t instanceof Error?t.message:String(t);d("error")?d("error").textContent=e:E(e),ee("Connection needs attention",!0)}async function qe(t,e){var l;if(!he("storage"))throw new Error("This host cannot keep recovery records. Copy the details before leaving; your pending request stays here.");const r=(t==null?void 0:t.requestId)||((l=e==null?void 0:e.active)==null?void 0:l.intent.requestId)||`edits-${e==null?void 0:e.target.instance}`,i=q.find(u=>u.id===r);if(i)return i;if(q.length>=8)throw new Error("Eight recovery records are already kept. Copy these details and resolve an earlier record first.");const n={id:r,savedAt:Date.now(),...t?{intent:structuredClone(t)}:{},...e?{journal:structuredClone(e)}:{}},a=[n,...q];return await L.setItem("napcraft-kept-saves-v1",JSON.stringify(a)),q=a,n}function Nt(){var t;_(`${B()}<span class="stamp">NOTHING THROWN AWAY</span><h2>Kept recovery records.</h2><p>These copies are never sent automatically. Recover a missing world with its code, or deliberately choose a separate new world.</p><div id="records"></div><button id="back" class="linkish">← World menu</button>`);for(const e of q){const r=document.createElement("button");r.className="recent",r.textContent=e.code?"Recovered world · View record":((t=e.intent)==null?void 0:t.operation)==="createWorld"?"Unresolved world creation":"Block recovery copy",r.onclick=()=>U(e),d("records").append(r)}d("back").onclick=O}function U(t){var u,h;const e=t?t.intent:x,r=t?t.journal:H,i=(e==null?void 0:e.operation)==="createWorld",n=!!(e&&e.expiresAt<=Date.now()/1e3),a=i?String(((u=e.input)==null?void 0:u.name)||"Your world"):"Your blocks",l=i?n?"This older request can no longer be safely retried. Your world might already exist, but its code never arrived. The original request is kept; creating a replacement could make a duplicate.":"The world code has not arrived yet. Check your host or signer for an approval prompt. Retrying uses the exact same saved request, so a lost reply will not create a second world.":`${(r==null?void 0:r.queue.length)||1} block or setting change(s) need review. We will check the saved world and retry only when it is safe. A recovery copy is kept before reviewing older work.`;if(_(`${B()}<span class="stamp">LET’S PICK UP WHERE YOU LEFT OFF</span><h2>${t!=null&&t.code?"Recovered world.":i?"Let’s find your world.":"Your unfinished save."}</h2><p><b>${ve(a)}</b></p><p>${t!=null&&t.code?"This request was linked to the world code you recognized. Its original details are retained.":l}</p>${!t&&(!i||!n)?'<button id="retrySave" class="primary wide">'+(i?"Retry saved request":"Review and retry safely")+"</button>":""}${i&&!(t!=null&&t.code)?'<label for="recoveredCode">Already have this world’s code?</label><input id="recoveredCode" class="code" placeholder="NC1-…" autocomplete="off"/><button id="checkCode" class="wide">Check recovered world</button>':""}${i&&n&&!t?'<button id="keepSave" class="wide">Keep request and continue</button>':""}<p id="error" class="error" role="status"></p><details><summary>Recovery details to keep or share with support</summary><textarea id="recoveryDetails" class="code" readonly rows="6">${ve(JSON.stringify(t||{intent:e,journal:r},null,2))}</textarea></details><p class="footnote">${t?"You can join an existing world or use practice from World menu.":"Nothing is discarded when you close this panel."}</p><button id="back" class="linkish">← World menu</button>`),d("back").onclick=O,((h=t==null?void 0:t.intent)==null?void 0:h.operation)==="createWorld"&&!t.code){const s=document.createElement("button");s.id="differentWorld",s.className="wide",s.textContent="Create a different world…",s.onclick=()=>it(!0),d("back").before(s)}d("retrySave")&&(d("retrySave").onclick=()=>void(async()=>{if(!I){if(i){await Ye();return}try{(r||e)&&await qe(e,r),await(x?Ye():It())}catch(s){z(s)}}})()),d("keepSave")&&(d("keepSave").onclick=()=>void(async()=>{if(!(I||!e||x!==e))try{await qe(e),await L.setItem("napcraft-pending-v1","null"),x=void 0,O(),E("Request kept. You can join another island or practice while its creation is unresolved.")}catch(s){z(s)}})()),d("checkCode")&&(d("checkCode").onclick=()=>void(async()=>{if(!(I||!e)){A(!0);try{const s=d("recoveredCode").value.trim(),o=We(s);if(o.release!==e.target.release)throw new Error("That code is from a different world release. Keep this request and check the original world code.");const c=await xe(o);_(`${B()}<h2>Is this your missing world?</h2><p><b>${ve(c.result.name)}</b><br>${c.result.edits} saved edits.</p><p>Only continue if you recognize this as the world you were creating. We will retain the original request and open this code.</p><button id="useRecovered" class="primary wide">Yes, open this world</button><button id="back" class="wide">Keep looking</button><p id="error" class="error"></p>`),d("back").onclick=()=>U(t),d("useRecovered").onclick=()=>void(async()=>{if(!I){A(!0);try{const y=t||await qe(e),p=q.map(w=>w.id===y.id?{...w,code:s}:w);await L.setItem("napcraft-kept-saves-v1",JSON.stringify(p)),q=p,x===e&&(await L.setItem("napcraft-pending-v1","null"),x=void 0),$=[{code:s,name:c.result.name},...$.filter(w=>w.code!==s)].slice(0,8),await j(),await de(o,c.result,c.revision)}catch(y){z(y)}finally{A(!1)}}})()}catch(s){z(s)}finally{A(!1)}}})())}function A(t){I=t;for(const e of["confirmCreate","confirmJoin","place","remove","retrySave","checkCode","keepSave","useRecovered"])d(e)&&(d(e).disabled=t);for(const[e,r]of[["confirmCreate","Create shared world"],["retrySave","Retry saved request"]])d(e)&&(t||e==="confirmCreate")&&(d(e).textContent=t?"Waiting for approval or reply…":r)}async function Mt(t){return J.invoke(t)}async function Lt(){var t;if(!I){if(V()){E("Resolve the unfinished save before creating another world.");return}if(q.some(e=>{var r;return((r=e.intent)==null?void 0:r.operation)==="createWorld"&&!e.code})&&!((t=d("separateWorld"))!=null&&t.checked)){z(new Error("Confirm that you want a separate world. Your earlier request will remain in Kept recovery records."));return}A(!0);try{const e=await J.describe(le);if(e.disabled||!e.active)throw new Error("Shared world hosting is not active yet. Try practice, or retry shortly.");x=J.intent({module:le,release:e.active},"createWorld",{name:d("name").value.trim()||"Our little island"}),await j(!0),await De()}catch(e){x&&U(),z(e)}finally{A(!1)}}}async function De(){if(!x)return;const t=x,e=M;if(t.expiresAt<=Date.now()/1e3||t.operation==="editBlock"){if(t.target.instance){const a=await xe(t.target);if(e!==M)return;x=void 0,await j(),await de(t.target,a.result,a.revision),E(t.operation==="editBlock"?"Older pending save refreshed. Review your last block before editing again.":"Retry expired. World refreshed; check your last block before editing again.");return}throw new Error("Creation response expired before its world code was recovered. Do not create a duplicate without checking with the provider.")}const r=await Mt(t);if(e!==M)return;const i={module:le,release:r.release,instance:r.instance},n=be(i);$=[{code:n,name:r.result.name},...$.filter(a=>a.code!==n)].slice(0,8),await j(!0),x=void 0,await j(),await de(i,r.result,r.revision),rt(),ee("✓ All blocks saved")}async function Ye(){if(!(I||!x)){A(!0);try{await De()}catch(t){U(),z(t)}finally{A(!1)}}}async function $e(t){if(!I){A(!0);try{if(V())throw new Error("Retry your unfinished save before changing worlds.");const e=We(t),r=await xe(e);$=[{code:t,name:r.result.name},...$.filter(i=>i.code!==t)].slice(0,8),await j(),await de(e,r.result,r.revision)}catch(e){z(e)}finally{A(!1)}}}function $t(t,e){if(!t||!Array.isArray(t.blocks)||t.blocks.length!==3888)throw new Error("Invalid world snapshot.");e<Le||(f.world=t,Le=e,d("worldName").textContent=t.name,d("app").dataset.revision=String(e),ee(t.locked?"Building paused by owner":"✓ All blocks saved"))}async function Ce(){M++,g==null||g.dispose(),g=void 0,f.pendingCells.clear(),clearInterval(et),clearInterval(tt),f.peers.clear(),D.clear(),Q=!1;const t=P;if(P="",t)try{await Z.close(t,"Leaving world")}catch{}f.keys.clear()}async function de(t,e,r,i){var s,o;const n=R==="shared"&&(k==null?void 0:k.instance)===t.instance&&(k==null?void 0:k.release)===t.release;await Ce(),k=t,R="shared",Le=-1,$t(e,r);const a=M;let l=!1;try{const y=(await J.describe(le,t.release)).schemas;l=!!((s=y==null?void 0:y.operations)!=null&&s.editBlocks&&y.operations.readUpdates)}catch{}if(a!==M)return;const u=await Fe.registry.call("soy.rooms.v1","soy_session",{},{timeoutMs:15e3}),h=(o=u.structuredContent)==null?void 0:o.actor;if(u.isError||typeof h!="string"||!/^[a-f0-9]{64}$/.test(h))throw new Error("Could not verify your player session. Rejoin before saving blocks.");if(a===M){Ve=h,n||(f.player={x:6.5+parseInt(h.slice(0,4),16)%5*1.3,z:8.5+parseInt(h.slice(4,8),16)%4*1.3,color:parseInt(h.slice(8,12),16)%6,name:"You"},Q=!0),g=new Ie(t,e,r,l,J,{persist:qt,saved:()=>{rt()},notice:E,change(c){if(a!==M)return;f.world=c.world,f.pendingCells=c.cells,f.confirmedEdits=c.confirmedEdits,d("app").dataset.revision=String(c.revision),d("app").dataset.pending=String(c.pending),d("app").dataset.sync=c.state;const y=c.state==="retry"||c.state==="refreshing"?c.message||"Refreshing saved blocks…":c.pending?`${c.pending} block${c.pending===1?"":"s"} pending · Saving…`:c.world.locked?"Building paused by owner":"✓ All blocks saved";ee(y,c.state==="retry")}},i,h),g.start(),nt(),d("share").hidden=!1,d("worldMode").textContent="Shared island · Creative";try{const c=await Z.open({scope:{type:"room",room:`napcraft-${t.instance}`},channel:"garden",protocol:Tt});if(a!==M){await Z.close(c.session.id,"World changed");return}P=c.session.id,F="Waiting for friends"}catch(c){F="Live players unavailable",d("worldMode").textContent="Shared · Live players unavailable",E(`Blocks are saved. Live players unavailable: ${c instanceof Error?c.message:"Connection denied"}`)}et=window.setInterval(()=>{document.hidden||Ot()},2600),tt=window.setInterval(()=>void st(),110)}}async function zt(){if(V()){E("Retry your unfinished save before changing worlds.");return}await Ce(),k=void 0,R="practice",f.world=Ue(),f.player={x:8,z:11,color:0,name:"You"},d("worldName").textContent="Practice island",d("worldMode").textContent="Creative · Temporary",d("share").hidden=!0,ee("Practice only · Changes are not saved"),nt()}function nt(){f.active=!0,d("playHud").hidden=!1,d("overlay").hidden=!0,f.canvas.focus()}async function Ot(){R==="shared"&&!I&&!x&&(g==null||g.sync())}function ue(t){if(I||R==="lobby"||!d("overlay").hidden)return;if(Q=!1,x){E("Your last save needs a retry. Open World menu.");return}if(f.world.locked){E("The owner has paused building.");return}const{x:e,z:r}=f.selected,i=ce(f.world,e,r)+(t?0:1);if(i<2){E("Keep the island’s foundation.");return}if(i>=12){E("You reached the sky limit — 12 blocks high.");return}const n=f.world.blocks[S(e,i,r)],a=t?0:Me;if(R==="practice")f.world.blocks[S(e,i,r)]=a,f.world.edits++;else if(!(g!=null&&g.enqueue({x:e,y:i,z:r,expected:n,block:a})))return;f.burst(e,i,r,t?n:Me)}async function st(){if(!P||Te||!D.size)return;Te=!0;const t=M;try{await Z.send(P,{v:1,seq:++At,x:f.player.x,z:f.player.z,color:f.player.color})}catch{t===M&&(F="Live connection interrupted")}finally{Te=!1}}let we;try{we=Z.onEvent(t=>{if(t.sessionId===P)if(t.type==="message"){const e=t.payload;if((e==null?void 0:e.v)===1&&e.kind==="saved"){D.has(t.from)&&e.instance===(k==null?void 0:k.instance)&&e.release===(k==null?void 0:k.release)&&(g==null||g.sync(!0));return}if(!e||!D.has(t.from)||e.v!==1||!Number.isSafeInteger(e.seq)||typeof e.x!="number"||typeof e.z!="number"||!Number.isFinite(e.x)||!Number.isFinite(e.z)||e.x<0||e.x>=18||e.z<0||e.z>=18||!Number.isInteger(e.color)||Number(e.color)<0||Number(e.color)>5)return;const r=f.peers.get(t.from);if(r&&Number(e.seq)<=r.seq||!r&&f.peers.size>=8)return;if(f.peers.set(t.from,{x:(r==null?void 0:r.x)??e.x,z:(r==null?void 0:r.z)??e.z,tx:e.x,tz:e.z,color:Number(e.color),name:`FRIEND ${t.from.slice(-4).toUpperCase()}`,seen:performance.now(),seq:Number(e.seq)}),Q&&Ve>t.from&&Math.hypot(f.player.x-e.x,f.player.z-e.z)<1.2)for(let i=0;i<20;i++){const n=6.5+i%5*1.3,a=8.5+Math.floor(i/5)*1.3;if([...f.peers.values()].every(l=>Math.hypot(n-l.tx,a-l.tz)>=1.2)){f.player.x=n,f.player.z=a;break}}}else t.type==="peer"?t.state==="joined"?(D.add(t.pubkey),f.peers.delete(t.pubkey),F="Live",st(),g==null||g.sync(!0)):t.state==="left"&&(D.delete(t.pubkey),f.peers.delete(t.pubkey),D.size||(F="Waiting for friends")):t.type==="closed"&&(P="",F="Live disconnected",d("worldMode").textContent="Shared · Live players disconnected",f.peers.clear(),D.clear(),ee("Live players disconnected · Rejoin from World menu",!0))})}catch{}function Wt(){k&&(_(`${B()}<span class="stamp">SAVE THIS LITTLE INVITATION</span><h2>Room for your friends.</h2><section id="listingControl" class="listing-control"></section><label for="shareCode">Or share your persistent world code</label><input id="shareCode" class="code" readonly value="${ve(be(k))}"/><button id="selectCode" class="primary wide">Select code to copy</button><p class="footnote">Copy with your device’s Copy command. Keep the code to return later. Anyone with it can edit. Worlds are public.</p><button id="lock" class="linkish">${f.world.locked?"Resume":"Pause"} building (owner only)</button>`),Et(d("listingControl"),be(k),f.world.name),d("selectCode").onclick=()=>{d("shareCode").focus(),d("shareCode").select()},d("lock").onclick=async()=>{if(I||V()||!k){E("Let pending blocks save before changing building permissions.");return}A(!0),x=J.intent(k,"setLocked",{locked:!f.world.locked}),await j();try{await De(),fe()}catch(t){t instanceof G&&t.definitive&&(x=void 0,await j()),z(t)}finally{A(!1)}})}d("help").onclick=()=>_(`${B()}<span class="stamp">MAKE YOURSELF AT HOME</span><h2>A few little tricks.</h2><div class="help-grid"><b>Wander</b><span>WASD / arrow keys, or the on-screen arrows. Friends have colored gardeners and name tags.</span><b>Desktop</b><span>Hover to aim. Click any visible block face to add on top of its column. Right-click erases the top block. Shift-click aims without building.</span><b>Touch</b><span>Tap a block to aim, then use Build or Erase. Drag to pan without building.</span><b>Keys</b><span>Space builds, X erases. Pick a material with 1–6 or the swatches.</span><b>Look around</b><span>Drag to pan. Use + / − to zoom.</span><b>Come back</b><span>Shared blocks save online. Keep your world code. Practice is temporary.</span></div><button id="helpBack" class="primary wide" style="margin-top:20px">Got it</button>`);d("help").addEventListener("click",()=>{d("helpBack").onclick=()=>R==="lobby"?O():fe()});d("panel").addEventListener("keydown",t=>{if(t.key!=="Tab")return;const e=[...d("panel").querySelectorAll("button:not(:disabled),input")],r=e[0],i=e.at(-1);t.shiftKey&&document.activeElement===r?(t.preventDefault(),i==null||i.focus()):!t.shiftKey&&document.activeElement===i&&(t.preventDefault(),r==null||r.focus())});d("worldMenu").onclick=O;d("share").onclick=Wt;for(const[t,e]of[["place",!1],["remove",!0]]){const r=d(t);let i=-1/0;r.onpointerdown=n=>{n.pointerType!=="touch"&&n.pointerType!=="pen"||(n.preventDefault(),i=performance.now(),ue(e))},r.onclick=n=>{n.detail&&performance.now()-i<800||ue(e)}}d("zoomIn").onclick=()=>f.zoom=Math.min(2.2,f.zoom+.15);d("zoomOut").onclick=()=>f.zoom=Math.max(.65,f.zoom-.15);const ot=document.querySelector(".materials");for(let t=1;t<=6;t++){const e=document.createElement("button");e.className="material",e.setAttribute("aria-label",ae[t].name),e.innerHTML=`<kbd>${t}</kbd><span class="swatch" style="background:${ae[t].color}"></span><small>${ae[t].name}</small>`,e.onclick=()=>Pe(t),ot.append(e)}function Pe(t){Me=t,ot.querySelectorAll("button").forEach((e,r)=>e.setAttribute("aria-pressed",String(r+1===t)))}Pe(4);for(const t of document.querySelectorAll("[data-key]")){const e=t.dataset.key;t.onpointerdown=r=>{r.preventDefault(),t.setPointerCapture(r.pointerId),f.keys.add(e),Q=!1};for(const r of["pointerup","pointercancel","lostpointercapture"])t.addEventListener(r,()=>f.keys.delete(e))}document.addEventListener("keydown",t=>{if(!d("overlay").hidden){t.key==="Escape"&&fe();return}/^[1-6]$/.test(t.key)&&Pe(Number(t.key)),["KeyW","KeyA","KeyS","KeyD","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.code)&&(f.keys.add(t.code),Q=!1,t.preventDefault()),t.code==="Space"&&(t.preventDefault(),t.repeat||ue(!1)),t.code==="KeyX"&&!t.repeat&&ue(!0),t.key==="Escape"&&O()});document.addEventListener("keyup",t=>f.keys.delete(t.code));window.addEventListener("blur",()=>f.keys.clear());window.addEventListener("online",()=>{g!=null&&g.uncertain?g.retry():g==null||g.sync(!0)});document.addEventListener("visibilitychange",()=>{f.keys.clear(),document.hidden||(g!=null&&g.uncertain?g.retry():g==null||g.sync(!0))});let C;f.canvas.onpointerdown=t=>{C||!f.active||!d("overlay").hidden||[0,1,2].includes(t.button)&&(t.preventDefault(),f.canvas.focus(),f.canvas.setPointerCapture(t.pointerId),C={id:t.pointerId,x:t.offsetX,y:t.offsetY,px:f.pan.x,py:f.pan.y,moved:!1,button:t.button,pointerType:t.pointerType})};f.canvas.onpointermove=t=>{if(!C){t.pointerType==="mouse"&&f.active&&d("overlay").hidden&&(f.canvas.style.cursor=f.pick(t.offsetX,t.offsetY)?"crosshair":"grab");return}if(C.id!==t.pointerId)return;const e=t.offsetX-C.x,r=t.offsetY-C.y;Math.hypot(e,r)>8&&(C.moved=!0),C.moved&&(f.canvas.style.cursor="grabbing",f.pan.x=Math.max(-600,Math.min(600,C.px+e)),f.pan.y=Math.max(-400,Math.min(400,C.py+r)))};f.canvas.onpointerup=t=>{if(!C||C.id!==t.pointerId)return;const e=C;C=void 0,!e.moved&&Math.hypot(t.offsetX-e.x,t.offsetY-e.y)<=8&&e.button!==1&&f.pick(t.offsetX,t.offsetY)&&e.pointerType==="mouse"&&!t.shiftKey&&ue(e.button===2),f.canvas.style.cursor="grab"};f.canvas.onpointercancel=()=>C=void 0;f.canvas.onlostpointercapture=()=>C=void 0;f.canvas.oncontextmenu=t=>t.preventDefault();window.setInterval(()=>{d("presence").textContent=R==="shared"?f.peers.size?`${f.peers.size+1} building together`:F:"CREATIVE CLUB",matchMedia("(pointer: coarse)").matches?d("hint").textContent="Tap a block to aim · Pick a color · Build":innerWidth<600?d("hint").textContent="Click to build · Right-click to erase":d("hint").textContent="Click to build · Right-click to erase · WASD to wander · Drag to pan"},500);async function Dt(){try{he("identity")&&(ge=await se.getPublicKey(),me=se.onChanged(()=>{(async()=>{const t=await se.getPublicKey();t!==ge&&(ge=t,await Ce(),R="lobby",f.active=!1,k=void 0,d("playHud").hidden=!0,d("share").hidden=!0,await Ke(),O())})()}))}catch{}await Ke(),O(),d("app").dataset.ready="true"}Dt();window.addEventListener("pagehide",()=>{Ce(),we==null||we.close(),me==null||me.close(),f.dispose()});
</script>
    <style>:root{font-family:ui-rounded,Avenir Next,Avenir,system-ui,sans-serif;color:#463656;background:#dedcf5;font-synthesis:none;--ink: #463656;--purple: #7657a8;--paper: #fffaf7;--line: #58416e24;--napplet-text-selection: none}*{box-sizing:border-box}html,body,#app{height:100%;width:100%;margin:0;overflow:hidden}#app{position:relative;isolation:isolate}button,input{font:inherit}button{cursor:pointer;touch-action:manipulation;border:1px solid var(--line);color:var(--ink);font-weight:750;background:var(--paper);border-radius:12px;min-height:44px;padding:10px 17px;box-shadow:0 3px #58416e18;transition:transform .12s,background .12s}button:active{transform:translateY(2px);box-shadow:none}button:hover{background:#f0e8fb}button:disabled{opacity:.55;cursor:wait}button:focus-visible,input:focus-visible,canvas:focus-visible{outline:3px solid #a084d2;outline-offset:3px}button.primary{background:#7657a8;color:#fff;border-color:#60458e;box-shadow:0 4px #513679}button.primary:hover{background:#8767ba}button.icon{width:44px;padding:8px;font-size:20px}button.small{font-size:12px;padding:8px 13px}button[hidden],[hidden]{display:none!important}#scene{display:block;width:100%;height:100%;touch-action:none;outline:none}header.hud{position:absolute;top:max(16px,env(safe-area-inset-top));left:20px;right:20px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px;pointer-events:none}.hud>*{pointer-events:auto}.world-chip{display:flex;align-items:center;gap:12px;text-align:left;padding:10px 14px;background:#fffaf7ed;max-width:48%;font-size:13px}.world-chip b{display:block;font-weight:850;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:230px}.world-chip small{display:block;font-size:10px;font-weight:600;opacity:.7;margin-top:3px}.logo{display:grid;place-items:center;background:#a186ca;color:#fff;border-radius:8px;font-size:28px;width:37px;height:37px}.top-right{display:flex;gap:8px;align-items:center}.pill{font-size:10px;letter-spacing:.08em;font-weight:800;background:#fffaf7b8;border:1px solid #ffffff7a;padding:10px 14px;border-radius:40px}.save-pill{position:absolute;top:91px;left:24px;font-size:11px;font-weight:700;color:#645274;max-width:60%;background:#fffaf7ad;padding:6px 10px;border-radius:8px}.zoom{position:absolute;right:20px;top:97px;display:flex;gap:6px}.zoom button{background:#fffaf7c9}.bottom{position:absolute;bottom:max(18px,env(safe-area-inset-bottom));left:50%;transform:translate(-50%);width:min(620px,calc(100% - 32px));display:flex;align-items:center;flex-direction:column;gap:11px;pointer-events:none}.bottom>*{pointer-events:auto}.hint{font-size:11px;font-weight:650;background:#fffaf7bf;padding:7px 13px;border-radius:20px;white-space:nowrap}.materials{display:flex;align-items:center;gap:6px;background:#fffaf7ed;border:1px solid #ffffff;padding:8px;border-radius:17px;box-shadow:0 5px 25px #6651821a}.material{width:68px;height:64px;padding:5px 4px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:4px;border:2px solid transparent;box-shadow:none;background:transparent;position:relative}.material[aria-pressed=true]{border-color:var(--purple);background:#ede3f8}.material span.swatch{display:block;height:24px;width:25px;border-radius:4px;box-shadow:inset -7px -6px #46365624,0 2px #46365620;transform:rotate(-6deg)}.material small{font-size:9px;font-weight:750}.material kbd{position:absolute;top:2px;right:4px;font-size:8px;opacity:.5}.control-row{display:flex;justify-content:space-between;align-items:center;width:100%;gap:20px}.dpad{display:grid;grid-template-columns:repeat(3,44px);grid-template-rows:repeat(2,42px);gap:3px}.dpad button{padding:4px;min-height:42px;font-size:20px;background:#fffaf7ca;touch-action:none}.dpad button:first-child{grid-column:2}.dpad button:nth-child(2){grid-row:2;grid-column:1}.dpad button:nth-child(3){grid-row:2;grid-column:2}.dpad button:nth-child(4){grid-row:2;grid-column:3}.build-actions{display:flex;gap:8px;align-self:flex-end;padding-bottom:3px}.build-actions button{min-height:51px;font-size:16px;padding:10px 22px}.erase{background:#fffaf7db}.overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;padding:85px 20px 20px;background:#dcd7eb25;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}.panel{width:min(430px,100%);max-height:100%;overflow:auto;border:2px solid #ffffff;border-radius:24px;background:#fffaf7f5;box-shadow:0 18px 70px #53436c25;padding:28px}.panel.directory-panel{width:min(650px,100%)}.lobby-heading{display:flex;align-items:center;gap:18px;justify-content:space-between;margin-bottom:20px}.lobby-heading h1{font-size:29px;margin:0}.lobby-heading button{flex-shrink:0;min-height:46px}.directory-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.panel .directory-heading h2{font-size:19px;margin:0;letter-spacing:-.02em}.directory-refresh{min-height:44px;padding:8px 12px;font-size:12px}.panel .directory-caption{font-size:12px;margin:5px 0 12px}.panel .directory-status{font-size:12px;line-height:1.5;margin:8px 0 12px;color:#746484}.world-list{display:grid;gap:8px;margin-bottom:16px}.world-row{display:flex;align-items:center;gap:12px;padding:12px;background:#f0eaf5;border:1px solid #e6dbef;border-radius:15px}.world-emblem{display:grid;place-items:center;background:#b19acb;color:#fff;width:38px;height:38px;border-radius:10px;font-size:25px;flex-shrink:0}.world-copy{display:grid;gap:4px;min-width:0;flex:1}.world-copy strong{font-size:14px;line-height:1.3;overflow-wrap:anywhere}.world-copy span{font-size:11px;color:#746484}.world-join{min-width:58px;min-height:44px;padding:8px 12px;flex-shrink:0}.panel .recents-heading{font-size:12px;margin:20px 0 8px}.listing-control{padding:14px;background:#f0eaf5;border-radius:15px;margin:0 0 18px}.listing-control h3{font-size:16px;margin:0 0 8px}.panel .listing-control p{font-size:12px;line-height:1.5}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:550px){.lobby-heading{flex-direction:column;align-items:stretch;gap:12px}.lobby-heading h1{font-size:27px}.world-row{gap:8px;padding:10px}.world-emblem{display:none}}.stamp{display:inline-block;color:#8864af;letter-spacing:.13em;font-size:10px;font-weight:850;margin-bottom:10px}.panel h1,.panel h2{font-size:33px;line-height:1.06;letter-spacing:-.035em;margin:0 0 13px;font-weight:850}.panel p{font-size:13px;line-height:1.65;color:#796782;margin:8px 0 19px}.panel label{display:block;font-size:11px;font-weight:750;margin-bottom:7px}.panel input{width:100%;min-height:45px;border:1px solid #d4c8df;border-radius:10px;background:#fff;color:var(--ink);padding:10px 12px;-webkit-user-select:text;user-select:text;margin-bottom:12px;font-size:14px}.panel input.code{font:11px ui-monospace,monospace}.panel textarea.code{width:100%;margin-top:10px;padding:10px;border:1px solid #d4c8df;border-radius:8px;resize:vertical;font:10px ui-monospace,monospace;-webkit-user-select:text;user-select:text}.panel details{font-size:11px;line-height:1.5}.panel summary{cursor:pointer}.panel .recovery-consent{display:flex;align-items:flex-start;gap:10px;line-height:1.5}.panel .recovery-consent input{width:22px;min-height:22px;flex-shrink:0}.panel .wide{width:100%;margin-bottom:10px}.panel .secondary-row{display:flex;gap:8px}.secondary-row button{flex:1;font-size:12px}.panel .footnote{font-size:10px;line-height:1.6;margin:14px 0 0}.panel .close{float:right;width:36px;min-height:36px;padding:4px;font-size:20px}.panel .recent{display:flex;width:100%;margin-top:8px;text-align:left;justify-content:space-between;font-size:12px}.panel .error{color:#a84663;font-size:12px;white-space:pre-wrap}.help-grid{display:grid;grid-template-columns:95px 1fr;gap:10px;font-size:12px;line-height:1.6}.help-grid b{color:#7657a8}.toast{position:absolute;left:50%;top:28%;transform:translate(-50%);background:#463656ef;color:#fff;padding:12px 19px;border-radius:13px;box-shadow:0 5px 20px #46365622;font-size:12px;max-width:85%;text-align:center;pointer-events:none;z-index:5}.tiny{display:none}.world-chip span:nth-child(2){min-width:0}input::placeholder{color:#ac9bb6}button.linkish{box-shadow:none;background:transparent;border:none;color:#8864af;font-size:12px;text-decoration:underline}.warning{color:#a84663!important}@media(min-width:1000px)and (min-height:650px){.bottom{bottom:28px}.panel{padding:34px}.panel h1{font-size:39px}.materials{padding:10px}.material{width:75px;height:70px}}@media(max-width:600px){header.hud{top:10px;left:10px;right:10px;gap:5px}.world-chip{max-width:58%;gap:7px;padding:8px;font-size:11px}.world-chip small{font-size:9px}.logo{width:28px;height:32px;font-size:25px}.pill{font-size:9px;padding:8px;letter-spacing:0}.top-right{gap:5px}.top-right .pill{position:absolute;top:100px;left:2px;max-width:calc(100% - 65px)}.save-pill{top:77px;left:12px;font-size:10px}.zoom{right:10px;top:76px;flex-direction:column}.zoom button{min-height:36px;height:36px;width:36px}.bottom{bottom:max(12px,env(safe-area-inset-bottom));width:calc(100% - 20px);gap:8px}.materials{gap:1px;padding:5px;width:100%;justify-content:center;border-radius:14px}.material{width:calc(100% / 6);height:58px;min-width:0}.material small{font-size:8px}.material kbd{display:none}.hint{font-size:10px;padding:6px 10px}.control-row{gap:8px}.build-actions{gap:5px}.build-actions button{padding:10px 12px;font-size:13px;min-width:60px}.dpad{grid-template-columns:repeat(3,44px);grid-template-rows:repeat(2,44px);gap:2px}.dpad button{min-height:44px}.overlay{padding:80px 13px 14px}.panel{padding:21px;border-radius:19px}.panel h1,.panel h2{font-size:30px}.panel p{font-size:12px}.panel .footnote{font-size:9px}.hint{max-width:100%;white-space:normal;text-align:center}.panel .secondary-row{gap:5px}.secondary-row button{padding:9px;font-size:11px}}@media(max-height:470px)and (min-width:600px){header.hud{top:8px}.world-chip{padding:6px 10px}.world-chip small{display:none}.logo{height:30px}.save-pill{top:65px}.bottom{bottom:10px;width:calc(100% - 40px)}.materials{position:absolute;bottom:0;left:50%;transform:translate(-50%);padding:4px}.material{width:51px;height:54px}.control-row{align-items:end}.hint{display:none}.zoom{top:70px}.overlay{padding:65px 15px 10px}.panel{padding:16px 25px}.panel h1{font-size:27px}.panel p{margin-bottom:10px}.dpad{grid-template-columns:repeat(3,44px)}.build-actions{flex-direction:column;gap:5px}.build-actions button{min-height:44px;padding:6px 13px}}@media(max-width:299px),(max-height:339px){.tiny{display:flex;position:absolute;top:0;right:0;bottom:0;left:0;z-index:20;background:#ebe5f6ef;padding:20px;align-items:center;justify-content:center;flex-direction:column;gap:10px;text-align:center}.tiny b{font-size:15px}.tiny span{font-size:11px;line-height:1.5}.hud,.overlay,#playHud{visibility:hidden}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
    <meta name="napplet-type" content="napcraft">
    <meta name="napplet-requires" content="cvm,webrtc">
    <meta name="napplet-config-schema" content="{&quot;type&quot;:&quot;object&quot;,&quot;title&quot;:&quot;Napcraft&quot;,&quot;properties&quot;:{},&quot;additionalProperties&quot;:false}">
  </head>
  <body>
    <main id="app">
      <canvas
        id="scene"
        tabindex="0"
        aria-label="Creative voxel island. WASD or arrows to walk. Click a block to build, right-click to erase. On touch, tap to aim then use Build or Erase. Space builds, X erases."
      ></canvas>
      <div class="tiny">
        <b>A little more room?</b
        ><span
          >Enlarge Napcraft to at least 300 × 340 to build. Your island will
          still be here.</span
        >
      </div>
      <header class="hud">
        <button id="worldMenu" class="world-chip" aria-label="World menu">
          <span class="logo">▧</span
          ><span
            ><b id="worldName">NAPCRAFT</b
            ><small id="worldMode">A little island. Big ideas.</small></span
          ><span>⌄</span>
        </button>
        <div class="top-right">
          <span id="presence" class="pill">CREATIVE CLUB</span
          ><button id="share" class="small" hidden>↗ Invite</button
          ><button id="help" class="icon" aria-label="How to play">?</button>
        </div>
      </header>
      <div id="playHud" hidden>
        <div class="save-pill" id="saveStatus" role="status">
          All blocks saved
        </div>
        <div class="zoom">
          <button id="zoomIn" class="icon" aria-label="Zoom in">+</button
          ><button id="zoomOut" class="icon" aria-label="Zoom out">−</button>
        </div>
        <div class="bottom">
          <div class="hint" id="hint">
            Click to build · Right-click to erase · WASD to wander · Drag to pan
          </div>
          <div
            class="materials"
            role="toolbar"
            aria-label="Building materials"
          ></div>
          <div class="control-row">
            <div class="dpad" aria-label="Movement">
              <button data-key="KeyW" aria-label="Walk up">↑</button
              ><button data-key="KeyA" aria-label="Walk left">←</button
              ><button data-key="KeyS" aria-label="Walk down">↓</button
              ><button data-key="KeyD" aria-label="Walk right">→</button>
            </div>
            <div class="build-actions">
              <button id="remove" class="erase">− <span>Erase</span></button
              ><button id="place" class="primary">+ <span>Build</span></button>
            </div>
          </div>
        </div>
      </div>
      <div id="overlay" class="overlay">
        <section
          id="panel"
          class="panel"
          role="dialog"
          aria-modal="true"
          aria-label="World menu"
        ></section>
      </div>
      <div
        id="toast"
        class="toast"
        role="status"
        aria-live="polite"
        hidden
      ></div>
    </main>
  </body>
</html>