feat: add hex trail puzzle, level editor, and visual design guide
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Shared geometry used by drawing AND swept collision detection. No host dependencies.
|
||||
export const PORTS = [0,1,2,3,4,5].map(d => ({x:Math.cos(-Math.PI/2+d*Math.PI/3)*Math.sqrt(3)/2,y:Math.sin(-Math.PI/2+d*Math.PI/3)*Math.sqrt(3)/2}));
|
||||
export function position(q,r,size=1){return{x:1.5*q*size,y:Math.sqrt(3)*(r+q/2)*size};}
|
||||
export function hexPoints(x,y,r){return Array.from({length:6},(_,i)=>({x:x+Math.cos(i*Math.PI/3)*r,y:y+Math.sin(i*Math.PI/3)*r}));}
|
||||
export function trackPoints(x,y,r,a,b){
|
||||
const p=PORTS[a],q=PORTS[b],points=[];
|
||||
for(let i=0;i<=20;i++){const t=i/20,u=1-t;points.push({x:x+r*(u*u*u*p.x+3*u*u*t*p.x*.28+3*u*t*t*q.x*.28+t*t*t*q.x),y:y+r*(u*u*u*p.y+3*u*u*t*p.y*.28+3*u*t*t*q.y*.28+t*t*t*q.y)});}
|
||||
return points;
|
||||
}
|
||||
export function pathPoints(route){
|
||||
const points=[];
|
||||
for(const s of route.steps){const c=position(s.q,s.r),p=PORTS[s.exit];if(s.enter===null)points.push(c,{x:c.x+p.x,y:c.y+p.y});else points.push(...trackPoints(c.x,c.y,1,s.enter,s.exit));}
|
||||
if(route.end)points.push(position(route.end.q,route.end.r));
|
||||
return points;
|
||||
}
|
||||
// Collider is the solid top face, not the decorative drop shadow or extruded side.
|
||||
export const COLLIDER_RADIUS = 0.88;
|
||||
const NORMALS=[{x:0,y:1},{x:Math.sqrt(3)/2,y:.5},{x:Math.sqrt(3)/2,y:-.5}];
|
||||
// Exact swept intersection for each rendered line segment against a Minkowski hexagon.
|
||||
// This avoids tunneling between animation frames, including grazing a neighboring tile.
|
||||
export function segmentHexHit(a,b,center,radius=COLLIDER_RADIUS*2){
|
||||
const limit=radius*Math.sqrt(3)/2;
|
||||
let enter=0,exit=1;
|
||||
for(const n of NORMALS){
|
||||
const value=(a.x-center.x)*n.x+(a.y-center.y)*n.y,delta=(b.x-a.x)*n.x+(b.y-a.y)*n.y;
|
||||
if(Math.abs(delta)<1e-10){if(Math.abs(value)>limit)return null;continue;}
|
||||
let lo=(-limit-value)/delta,hi=(limit-value)/delta;if(lo>hi)[lo,hi]=[hi,lo];
|
||||
enter=Math.max(enter,lo);exit=Math.min(exit,hi);if(enter>exit)return null;
|
||||
}
|
||||
return enter<=1&&exit>=0?Math.max(0,enter):null;
|
||||
}
|
||||
export function sweepPath(points,obstacles){
|
||||
for(let i=1;i<points.length;i++){
|
||||
const a=points[i-1],b=points[i];let first=null;
|
||||
for(const o of obstacles){
|
||||
if(o.x<Math.min(a.x,b.x)-1.8||o.x>Math.max(a.x,b.x)+1.8||o.y<Math.min(a.y,b.y)-1.8||o.y>Math.max(a.y,b.y)+1.8)continue;
|
||||
const t=segmentHexHit(a,b,o);
|
||||
if(t!==null&&(!first||t<first.t))first={t,blocker:o.id};
|
||||
}
|
||||
if(first){const point={x:a.x+(b.x-a.x)*first.t,y:a.y+(b.y-a.y)*first.t};return{...first,point,points:[...points.slice(0,i),point]};}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user