192 lines
5.7 KiB
JavaScript
192 lines
5.7 KiB
JavaScript
"use strict";
|
|
|
|
// ─── Path Network (orthogonal) ─────────
|
|
function genNet(li){
|
|
var cfg=LEVELS[li];
|
|
var cols=6+li*2, rows=8+li*3;
|
|
var gapX=100+rng(-10,10), gapY=80+rng(-10,10);
|
|
var offX=80, offY=80;
|
|
var mapW=offX*2+cols*gapX, mapH=offY*2+rows*gapY;
|
|
|
|
var grid=[];
|
|
var nodes=[];
|
|
var nid=0;
|
|
|
|
for(var r=0;r<=rows;r++){
|
|
grid[r]=[];
|
|
for(var c=0;c<=cols;c++){
|
|
var keep = r===0||r===rows||c===0||c===cols||
|
|
(r===rows&&c===Math.floor(cols/2))||
|
|
(r===0&&c===Math.floor(cols/2))||
|
|
(r===rows-1&&c<=1)||
|
|
(r===Math.floor(rows*0.4)&&c>=cols-1);
|
|
if(!keep && Math.random()<0.35){
|
|
grid[r][c]=null; continue;
|
|
}
|
|
var jx=(r>0&&r<rows&&c>0&&c<cols)?rng(-12,12):0;
|
|
var jy=(r>0&&r<rows&&c>0&&c<cols)?rng(-8,8):0;
|
|
var nd={x:offX+c*gapX+jx, y:offY+r*gapY+jy, id:nid, r:r, c:c, role:"path"};
|
|
nodes.push(nd);
|
|
grid[r][c]=nd;
|
|
nid++;
|
|
}
|
|
}
|
|
|
|
var edges=[];
|
|
var edgeSet={};
|
|
function addEdge(a,b){
|
|
if(!a||!b)return;
|
|
var k=Math.min(a.id,b.id)+"-"+Math.max(a.id,b.id);
|
|
if(edgeSet[k])return;
|
|
edgeSet[k]=true;
|
|
edges.push([a.id,b.id]);
|
|
}
|
|
|
|
for(var r=0;r<=rows;r++){
|
|
for(var c=0;c<=cols;c++){
|
|
if(!grid[r][c])continue;
|
|
if(c<cols&&grid[r][c+1]) addEdge(grid[r][c],grid[r][c+1]);
|
|
if(r<rows&&grid[r+1]&&grid[r+1][c]) addEdge(grid[r][c],grid[r+1][c]);
|
|
}
|
|
}
|
|
|
|
for(var d=0;d<2+li;d++){
|
|
var src=nodes[Math.floor(Math.random()*nodes.length)];
|
|
var dir=Math.floor(Math.random()*4);
|
|
var dx=[gapX,0,-gapX,0][dir], dy=[0,gapY,0,-gapY][dir];
|
|
var nx=src.x+dx+rng(-8,8), ny=src.y+dy+rng(-8,8);
|
|
if(nx>40&&nx<mapW-40&&ny>40&&ny<mapH-40){
|
|
var dn={x:nx,y:ny,id:nid,role:"deadend"};
|
|
nodes.push(dn); nid++;
|
|
edges.push([src.id,dn.id]);
|
|
}
|
|
}
|
|
|
|
var startC=Math.floor(cols/2);
|
|
var startN=grid[rows][startC];
|
|
if(!startN){
|
|
for(var cc=0;cc<=cols;cc++){
|
|
if(grid[rows][cc]){startN=grid[rows][cc];break;}
|
|
}
|
|
}
|
|
var visited={};
|
|
var queue=[startN.id];
|
|
visited[startN.id]=true;
|
|
var adj={};
|
|
edges.forEach(function(e){
|
|
if(!adj[e[0]])adj[e[0]]=[];
|
|
if(!adj[e[1]])adj[e[1]]=[];
|
|
adj[e[0]].push(e[1]);
|
|
adj[e[1]].push(e[0]);
|
|
});
|
|
while(queue.length){
|
|
var cur=queue.shift();
|
|
(adj[cur]||[]).forEach(function(nb){
|
|
if(!visited[nb]){visited[nb]=true;queue.push(nb);}
|
|
});
|
|
}
|
|
nodes.forEach(function(n){
|
|
if(visited[n.id])return;
|
|
var best=null,bd=Infinity;
|
|
nodes.forEach(function(m){
|
|
if(!visited[m.id])return;
|
|
var d=dist(n,m);
|
|
if(d<bd){bd=d;best=m;}
|
|
});
|
|
if(best){
|
|
edges.push([n.id,best.id]);
|
|
if(!adj[n.id])adj[n.id]=[];
|
|
if(!adj[best.id])adj[best.id]=[];
|
|
adj[n.id].push(best.id);
|
|
adj[best.id].push(n.id);
|
|
visited[n.id]=true;
|
|
}
|
|
});
|
|
|
|
var peakC=Math.floor(cols/2);
|
|
var peakN=grid[0][peakC]||nodes[0];
|
|
var labN=grid[rows-1]?grid[rows-1][0]||grid[rows][0]||nodes[1]:nodes[1];
|
|
if(labN.id===startN.id){
|
|
for(var ci=0;ci<=cols;ci++){
|
|
if(grid[rows]&&grid[rows][ci]&&grid[rows][ci].id!==startN.id){labN=grid[rows][ci];break;}
|
|
}
|
|
}
|
|
var apoRow=Math.floor(rows*0.4);
|
|
var apoN=grid[apoRow]?grid[apoRow][cols]||grid[apoRow][cols-1]||nodes[3]:nodes[3];
|
|
|
|
peakN.role="peak"; startN.role="start"; labN.role="lab"; apoN.role="apo";
|
|
|
|
return {nodes:nodes,edges:edges,mapW:mapW,mapH:mapH,
|
|
peak:peakN,start:startN,lab:labN,apo:apoN};
|
|
}
|
|
|
|
// ─── Vegetation ─────────
|
|
function genVeg(net){
|
|
var trees=[],bushes=[];
|
|
var segs=net.edges.map(function(e){return{a:net.nodes[e[0]],b:net.nodes[e[1]]};});
|
|
function nearPath(x,y,minD){
|
|
for(var i=0;i<segs.length;i++){
|
|
var s=segs[i],ax=s.a.x,ay=s.a.y,bx=s.b.x,by=s.b.y;
|
|
var dx=bx-ax,dy=by-ay,l2=dx*dx+dy*dy;
|
|
var t=l2===0?0:clamp(((x-ax)*dx+(y-ay)*dy)/l2,0,1);
|
|
var d=Math.sqrt((x-(ax+t*dx))**2+(y-(ay+t*dy))**2);
|
|
if(d<minD)return true;
|
|
}
|
|
return false;
|
|
}
|
|
for(var i=0;i<500;i++){
|
|
var vx=rng(15,net.mapW-15), vy=rng(15,net.mapH-15);
|
|
if(nearPath(vx,vy,45))continue;
|
|
if(Math.random()<0.5){
|
|
trees.push({x:vx,y:vy,sz:rng(14,30),shade:Math.floor(rng(0,4)),trunk:rng(10,18)});
|
|
}else{
|
|
bushes.push({x:vx,y:vy,sz:rng(6,16),shade:Math.floor(rng(0,3))});
|
|
}
|
|
}
|
|
return{trees:trees,bushes:bushes};
|
|
}
|
|
|
|
// ─── Stones on paths ────
|
|
function genStones(net,count,li){
|
|
var stones=[];
|
|
var cIdx=Math.floor(Math.random()*count);
|
|
var segs=net.edges.map(function(e){return{a:net.nodes[e[0]],b:net.nodes[e[1]]};});
|
|
for(var i=0;i<count;i++){
|
|
var pt,tries=0;
|
|
do{
|
|
var s=segs[Math.floor(Math.random()*segs.length)];
|
|
var t=rng(0.2,0.8);
|
|
pt={x:lerp(s.a.x,s.b.x,t),y:lerp(s.a.y,s.b.y,t)};
|
|
tries++;
|
|
}while(tries<100&&(
|
|
dist(pt,net.lab)<70||dist(pt,net.start)<60||dist(pt,net.peak)<70||dist(pt,net.apo)<70||
|
|
stones.some(function(s2){return dist(pt,s2)<55;})
|
|
));
|
|
var isC=i===cIdx;
|
|
stones.push({id:i,x:pt.x,y:pt.y,isCystine:isC,
|
|
color:isC?CYSTINE_COL:STONE_COLS[i%STONE_COLS.length],
|
|
scanned:false,rolling:false,shape:Math.floor(Math.random()*3),
|
|
rollAngle:0});
|
|
}
|
|
var cs=stones[cIdx];
|
|
var hints=[];
|
|
for(var h=0;h<3+li;h++){
|
|
hints.push({x:cs.x+rng(-65,65),y:cs.y+rng(-65,65),phase:rng(0,Math.PI*2)});
|
|
}
|
|
return{stones:stones,hints:hints};
|
|
}
|
|
|
|
// ─── Closest point on path network ────
|
|
function closestOnPaths(px,py,edges,nodes){
|
|
var best=Infinity,bx=px,by=py;
|
|
for(var i=0;i<edges.length;i++){
|
|
var a=nodes[edges[i][0]],b=nodes[edges[i][1]];
|
|
var dx=b.x-a.x,dy=b.y-a.y,l2=dx*dx+dy*dy;
|
|
var t=l2===0?0:clamp(((px-a.x)*dx+(py-a.y)*dy)/l2,0,1);
|
|
var cx=a.x+t*dx,cy=a.y+t*dy;
|
|
var d=(px-cx)**2+(py-cy)**2;
|
|
if(d<best){best=d;bx=cx;by=cy;}
|
|
}
|
|
return{x:bx,y:by,d:Math.sqrt(best)};
|
|
}
|