Spaces:
Running on Zero
Running on Zero
File size: 6,525 Bytes
623f6f8 24cc077 623f6f8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /* Browser-only preview of the selected saved track. No upload for preview. */
(function(root,factory){
if(typeof module==='object'&&module.exports)module.exports=factory();
else root.TrackMap=factory();
})(typeof window==='undefined'?this:window,function(){
const COLOURS=['#607e96','#b89561','#b9d9ed'];
const NAMES=['Asphalt','Dirt','Ice'];
const VECTOR=/^Vector2i\((-?\d+),\s*(-?\d+)\)$/;
function vector(value){
const match=typeof value==='string'&&VECTOR.exec(value);
if(!match)throw Error('Track has invalid tile coordinates');
const x=Number(match[1]),y=Number(match[2]);
if(!Number.isSafeInteger(x)||!Number.isSafeInteger(y)||Math.abs(x)>1000||Math.abs(y)>1000)
throw Error('Track coordinates exceed preview bounds');
return [x,y];
}
async function decode(file){
if(!file||file.size<1||file.size>1048576)throw Error('Choose a .track file under 1 MB');
if(typeof DecompressionStream!=='function')throw Error('This browser cannot preview gzip tracks');
const reader=file.stream().pipeThrough(new DecompressionStream('gzip')).getReader();
const chunks=[];let total=0;
try{
while(true){
const {done,value}=await reader.read();if(done)break;
total+=value.byteLength;
if(total>2*1024*1024)throw Error('Track preview exceeds the 2 MB expanded limit');
chunks.push(value);
}
}finally{reader.releaseLock();}
const bytes=new Uint8Array(total);let at=0;
for(const chunk of chunks){bytes.set(chunk,at);at+=chunk.length;}
return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));
}
function model(saved,atlas){
const entries=Object.entries(saved?.Tiles??{});
if(entries.length<1||entries.length>256)throw Error('Track has no previewable tiles');
const tiles=entries.map(([position,item])=>{
const [x,y]=vector(position);
const id=item?.Block?.TileSetId;
const block=atlas?.blocks?.[`${id?.AtlasId}:${id?.AlternativeId}`];
if(!block||![0,1,2].includes(block.surface))throw Error('Track uses a piece not in the simulator atlas');
return {x,y,surface:block.surface,connections:block.connections};
});
if(new Set(tiles.map(t=>`${t.x},${t.y}`)).size!==tiles.length)throw Error('Duplicate track tile');
const start=vector(saved.StartTileCoords);
const minX=Math.min(...tiles.map(t=>t.x)),maxX=Math.max(...tiles.map(t=>t.x));
const minY=Math.min(...tiles.map(t=>t.y)),maxY=Math.max(...tiles.map(t=>t.y));
if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large');
return {tiles,start,bounds:{minX,maxX,minY,maxY}};
}
function render(canvas,track){
const context=canvas.getContext('2d');
if(!context)throw Error('Canvas preview unavailable');
const w=canvas.width,h=canvas.height;
context.clearRect(0,0,w,h);
context.fillStyle='#0c1926';context.fillRect(0,0,w,h);
const b=track.bounds,columns=b.maxX-b.minX+1,rows=b.maxY-b.minY+1;
const size=Math.min(160,(w-70)/columns,(h-70)/rows);
const left=(w-columns*size)/2,top=(h-rows*size)/2;
for(const tile of track.tiles){
const x=left+(tile.x-b.minX)*size,y=top+(tile.y-b.minY)*size;
context.fillStyle=COLOURS[tile.surface];
context.beginPath();context.roundRect(x+2,y+2,size-4,size-4,Math.max(4,size*.13));context.fill();
context.strokeStyle='#d7e8f2';context.lineWidth=Math.max(1,size*.018);
context.stroke();
const edge=connection=>{
const offset=(connection.index-1)*size*.16;
switch(connection.side){
case 0:return [x+size/2+offset,y+2];
case 1:return [x+size/2+offset,y+size-2];
case 2:return [x+2,y+size/2+offset];
case 3:return [x+size-2,y+size/2+offset];
default:return [x+size/2,y+size/2];
}
};
if(Array.isArray(tile.connections)&&tile.connections.length>=2){
const a=edge(tile.connections[0]),z=edge(tile.connections[1]);
context.beginPath();context.moveTo(...a);
context.quadraticCurveTo(x+size/2,y+size/2,...z);
context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
context.lineCap='round';context.stroke();
}
}
const sx=left+(track.start[0]-b.minX+.5)*size,sy=top+(track.start[1]-b.minY+.5)*size;
context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2);
context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke();
}
function mount({document,fetch,picker}){
const canvas=document.getElementById('trackMap');
const status=document.getElementById('mapStatus');
const legend=document.getElementById('mapLegend');
if(!canvas||!status||!picker)return;
let version=0,atlasPromise=null;
async function refresh(){
const current=++version;
const info=picker.info();
const source=info.source==='builtin'?info.display_name:document.getElementById('track')?.files?.[0]?.name;
if(!source){status.textContent='Select a built-in track or choose a custom .track file.';legend.textContent='';renderEmpty();return;}
status.textContent='Loading '+source+'…';
try{
atlasPromise??=fetch('/assets/track-atlas.json',{cache:'no-store',credentials:'same-origin'}).then(r=>{
if(!r.ok)throw Error('Track atlas unavailable');return r.json();
});
const [file,atlas]=await Promise.all([picker.file(),atlasPromise]);
const track=model(await decode(file),atlas);
if(current!==version)return;
render(canvas,track);
const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++);
status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the start';
legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{
const i=NAMES.indexOf(name);return name+' '+counts[i];
}).join(' · ')+' · Piece and terrain preview; collision shapes are not shown.';
}catch(error){
if(current!==version)return;
renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
}
}
function renderEmpty(){const ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);ctx.fillStyle='#0c1926';ctx.fillRect(0,0,canvas.width,canvas.height);}
document.addEventListener('track-library-change',refresh);
refresh();
return {refresh};
}
return {mount,model,decode,render};
});
|