1import * as THREE from '../../assets/vendor/three.module.js'; 2const overlapping=(a,b)=>a.left<b.right+8&&a.right>b.left-8&&a.top<b.bottom+8&&a.bottom>b.top-8; 3export class ResidentDialogue{ 4 constructor(service){ 5 this.service=service;this.recent=[];this.ticket=0;this.cooldown=0;this.remaining=0;this.target=null; 6 this.balloon=document.createElement('aside');this.balloon.id='resident-balloon';this.balloon.hidden=true;this.balloon.setAttribute('aria-label','Robot greeting'); 7 this.words=document.createElement('p');this.words.setAttribute('role','status');this.words.setAttribute('aria-live','polite'); 8 this.close=document.createElement('button');this.close.type='button';this.close.textContent='Ã';this.close.setAttribute('aria-label','Dismiss robot greeting');this.balloon.append(this.words,this.close);document.body.append(this.balloon); 9 this.close.addEventListener('click',()=>this.hide()); 10 document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!this.balloon.hidden&&!document.querySelector('dialog[open]')){e.preventDefault();this.hide();}}); 11 document.addEventListener('visibilitychange',()=>{if(document.hidden)this.suspend();}); 12 } 13 hide(){this.ticket++;this.controller?.abort();this.controller=null;this.target=null;this.remaining=0;this.balloon.hidden=true;this.words.textContent='';} 14 suspend(){this.hide();} 15 forget(entry){if(this.target===entry)this.hide();} 16 async greet(entry){ 17 if(performance.now()<this.cooldown)return;const room=this.service.getRoom();if(!this.service.eligible(room,entry))return; 18 this.hide();this.target=entry;this.cooldown=performance.now()+3000;const ticket=this.ticket;this.controller=new AbortController(); 19 const facing=this.service.beginGreeting(entry,this.controller.signal); 20 try{ 21 const response=await fetch('robots.php?greet=1&recent='+encodeURIComponent(this.recent.join(',')),{credentials:'omit',cache:'no-store',signal:this.controller.signal}); 22 if(!response.ok)return;const data=await response.json(); 23 if(ticket!==this.ticket||this.target!==entry||!this.service.eligible(this.service.getRoom(),entry))return; 24 if(data.enabled===false){this.service.setEnabled(false);return;} 25 const p=data.phrase;if(!p||!Number.isSafeInteger(p.id)||p.id<1||typeof p.text!=='string'||!p.text.trim()||[...p.text].length>240||/[\u0000-\u001f\u007f]/.test(p.text))return; 26 if(!(await facing)||ticket!==this.ticket||this.target!==entry||!this.service.eligible(this.service.getRoom(),entry))return; 27 this.recent.push(p.id);if(this.recent.length>20)this.recent.shift();this.words.textContent=p.text;this.remaining=12;this.balloon.hidden=false;if(!this.position(this.balloon,room,entry))this.hide(); 28 }catch{/* Optional authored dialogue never blocks visiting. */} 29 } 30 position(node,room,entry,offset=1.35){ 31 const p=new THREE.Vector3(entry.descriptor.x,entry.descriptor.y+offset,entry.descriptor.z).project(room.camera),x=(p.x+1)*innerWidth/2,y=(1-p.y)*innerHeight/2; 32 const w=Math.min(node.offsetWidth,innerWidth-24),h=node.offsetHeight; 33 if(w<=0||h<=0||w>innerWidth-24||h>innerHeight-24)return false; 34 const avoid=[...document.querySelectorAll('.masthead,.room-heading,.visitor-bar,#movement,#viewing-controls,#annex-controls,#nearby-display,#reset-view,#map-toggle,#booth-toggle,#explorer-toggle')].filter(n=>!n.hidden&&n.getClientRects().length).map(n=>n.getBoundingClientRect()); 35 for(const [left,top] of [[x-w/2,y-h-12],[x+20,y-h/2],[x-w-20,y-h/2],[x-w/2,y+20]]){ 36 const l=Math.max(12,Math.min(innerWidth-w-12,left)),t=Math.max(12,Math.min(innerHeight-h-12,top)),rect={left:l,right:l+w,top:t,bottom:t+h}; 37 if(avoid.some(r=>overlapping(rect,r)))continue;node.style.left=l+'px';node.style.top=t+'px';return true; 38 } 39 return false; 40 } 41 update(room,dt){ 42 const due=(this.layoutDue=(this.layoutDue||0)-dt)<=0;if(due)this.layoutDue=.1; 43 if(this.target){ 44 if(this.target.state!==room.residents||(due&&!this.service.eligible(room,this.target,5))){this.hide();return;} 45 if(!this.balloon.hidden){if(due&&!this.position(this.balloon,room,this.target)){this.hide();return;} 46 if(!this.balloon.matches(':hover')&&!this.balloon.contains(document.activeElement))this.remaining-=dt; 47 if(this.remaining<=0)this.hide(); 48 } 49 } 50 } 51}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.