Geometry helper that builds routed link paths between squad skill cards. Runs a small Dijkstra over card-corner nodes, clips endpoints to card edges, offsets paths that would pass through cards, and picks a badge marker position along each route.
using System;
using System.Collections.Generic;
using Socer;
public sealed record SquadSkillRoute(SquadSkillConnection Connection,Point[] Points,Point Marker);
public static class SquadSkillLinkGeometry
{
const float HalfWidth=77.5f,HalfHeight=101.5f;
public static bool InsideCard(Point point,Point center,float padding=5)=>
Math.Abs(point.X-center.X)<HalfWidth+padding && Math.Abs(point.Y-center.Y)<HalfHeight+padding;
static bool Hits(Point a,Point b,Point center,float padding=6)
{
float low=0,high=1;var delta=b-a;
bool Axis(float origin,float direction,float min,float max)
{
if(Math.Abs(direction)<.001f)return origin>min && origin<max;
float x=(min-origin)/direction,y=(max-origin)/direction;
low=Math.Max(low,Math.Min(x,y));high=Math.Min(high,Math.Max(x,y));
return low<high;
}
return Axis(a.X,delta.X,center.X-HalfWidth-padding,center.X+HalfWidth+padding) &&
Axis(a.Y,delta.Y,center.Y-HalfHeight-padding,center.Y+HalfHeight+padding);
}
static Point Edge(Point center,Point toward)
{
var d=toward-center;
float t=Math.Min((HalfWidth+5)/Math.Max(.001f,Math.Abs(d.X)),(HalfHeight+5)/Math.Max(.001f,Math.Abs(d.Y)));
return center+d*Math.Min(1,t);
}
static Point Marker(Point[] path,Point[] centers,List<SquadSkillRoute> previous)
{
Point best=(path[0]+path[^1])*.5f;float score=float.MaxValue;
float length=0;for(int i=1;i<path.Length;i++)length+=(path[i]-path[i-1]).Length;
float travelled=0;
for(int i=1;i<path.Length;i++)
{
var a=path[i-1];var d=path[i]-a;float segment=d.Length;
for(int step=1;step<10;step++)
{
float fraction=step/10f;var point=a+d*fraction;
float penalty=Math.Abs(travelled+segment*fraction-length*.5f);
foreach(var center in centers)if(InsideCard(point,center,35))penalty+=10000;
foreach(var route in previous)if((point-route.Marker).Length<76)penalty+=1000;
if(penalty<score){score=penalty;best=point;}
}
travelled+=segment;
}
return best;
}
// Built only after a lineup/layout change. Straight segments go around cards;
// their endpoints stop at card edges.
public static SquadSkillRoute[] Build(SquadSkillConnection[] links,Point[] centers,float width,float height)
{
var corners=new List<Point>();
foreach(var center in centers)
foreach(int x in new[]{-1,1})foreach(int y in new[]{-1,1})
{
var point=center+new Point(x*(HalfWidth+8),y*(HalfHeight+8));
if(point.X>=12 && point.X<=width-12 && point.Y>=12 && point.Y<=height-12)corners.Add(point);
}
var routes=new List<SquadSkillRoute>();
foreach(var link in links)
{
var nodes=new List<Point>{centers[link.FirstSlot],centers[link.SecondSlot]};nodes.AddRange(corners);
int count=nodes.Count;var costs=new float[count];var visited=new bool[count];var parent=new int[count];
Array.Fill(costs,float.MaxValue);Array.Fill(parent,-1);costs[0]=0;
for(int iteration=0;iteration<count;iteration++)
{
int from=-1;float distance=float.MaxValue;
for(int i=0;i<count;i++)if(!visited[i] && costs[i]<distance){from=i;distance=costs[i];}
if(from<0 || from==1)break;visited[from]=true;
for(int to=1;to<count;to++)
{
if(to==from || visited[to])continue;
bool blocked=false;
for(int slot=0;slot<centers.Length;slot++)
{
if(slot==link.FirstSlot && from==0 || slot==link.SecondSlot && to==1)continue;
if(Hits(nodes[from],nodes[to],centers[slot])){blocked=true;break;}
}
if(blocked)continue;
float next=distance+(nodes[to]-nodes[from]).Length+8;
if(next<costs[to]){costs[to]=next;parent[to]=from;}
}
}
var path=new List<Point>();
if(parent[1]>=0){for(int node=1;node>=0;node=parent[node])path.Add(nodes[node]);path.Reverse();}
else{path.Add(nodes[0]);path.Add(nodes[1]);}
// A badge still needs room when two cards almost touch vertically.
if(path.Count==2 && Math.Abs(path[0].X-path[1].X)<HalfWidth*2+70 &&
Math.Abs(path[0].Y-path[1].Y)<HalfHeight*2+70)
{
var a=path[0];var b=path[1];
foreach(int side in new[]{1,-1})
{
var shift=new Point(side*(HalfWidth+46),0);
var candidate=new[]{a,a+shift,b+shift,b};bool clear=true;
for(int segment=1;segment<candidate.Length && clear;segment++)
{
if(candidate[segment].X<12 || candidate[segment].X>width-12){clear=false;break;}
for(int slot=0;slot<centers.Length;slot++)
{
if(slot==link.FirstSlot && segment==1 || slot==link.SecondSlot && segment==candidate.Length-1)continue;
if(Hits(candidate[segment-1],candidate[segment],centers[slot])){clear=false;break;}
}
}
if(clear){path=new List<Point>(candidate);break;}
}
}
path[0]=Edge(path[0],path[1]);path[^1]=Edge(path[^1],path[^2]);
var points=path.ToArray();routes.Add(new(link,points,Marker(points,centers,routes)));
}
return routes.ToArray();
}
}