A Sandbox UI Panel that paints an overhead soccer chalk diagram of a saved StrategyPlan. Draws the pitch, each starting player's position, phase routes as arrows, pass and shot action arrows, and defender/attacker dots on OnDraw.
using Sandbox;
using Sandbox.UI;
using Socer;
using System;
using System.Linq;
// Compact overhead chalk diagram for a saved play. It reads the plan only.
public sealed class StrategyTileCanvas : Panel
{
public StrategyPlan Plan {get;set;}
public override void OnDraw(Painter painter)
{
if(Plan?.Start?.Count!=5 || Plan.Defence?.Count!=5 || Plan.Phases==null)return;
using var scope=painter.Scope();painter.Scale(ScaleToScreen);
float w=Box.Rect.Width*ScaleFromScreen,h=Box.Rect.Height*ScaleFromScreen;
float left=14,top=12,fieldW=Math.Max(1,w-28),fieldH=Math.Max(1,h-24);
Vector2 At(Point a)=>new(left+a.X/105*fieldW,top+a.Y/68*fieldH);
void Line(Painter pen,Point a,Point b,Color color,float width)
{pen.Stroke=Stroke.Solid(color,width);pen.Line(At(a),At(b));}
void Dot(Painter pen,Point at,Color color,float radius)
{
for(int i=0;i<12;i++)
{
float a=i*MathF.PI/6,b=(i+1)*MathF.PI/6;
var first=At(at)+new Vector2(MathF.Cos(a),MathF.Sin(a))*radius;
var second=At(at)+new Vector2(MathF.Cos(b),MathF.Sin(b))*radius;
pen.Stroke=Stroke.Solid(color,2);pen.Line(first,second);
}
}
void Arrow(Painter pen,Point a,Point b,Color color,float width)
{
if((b-a).Length<.2f)return;
Line(pen,a,b,color,width);
var direction=(b-a).Unit;var side=new Point(-direction.Y,direction.X);
Line(pen,b,b-direction*2.2f+side*1.1f,color,width);
Line(pen,b,b-direction*2.2f-side*1.1f,color,width);
}
var field=new Color(.72f,.84f,.72f,.68f);
Line(painter,new(0,0),new(105,0),field,1.4f);Line(painter,new(105,0),new(105,68),field,1.4f);
Line(painter,new(105,68),new(0,68),field,1.4f);Line(painter,new(0,68),new(0,0),field,1.4f);
Line(painter,new(52.5f,0),new(52.5f,68),field,1.1f);
Line(painter,new(0,14),new(16.5f,14),field,1);Line(painter,new(16.5f,14),new(16.5f,54),field,1);Line(painter,new(16.5f,54),new(0,54),field,1);
Line(painter,new(105,14),new(88.5f,14),field,1);Line(painter,new(88.5f,14),new(88.5f,54),field,1);Line(painter,new(88.5f,54),new(105,54),field,1);
for(int i=0;i<24;i++)
{float a=i*MathF.PI/12,b=(i+1)*MathF.PI/12;Line(painter,new(52.5f+9.15f*MathF.Cos(a),34+9.15f*MathF.Sin(a)),new(52.5f+9.15f*MathF.Cos(b),34+9.15f*MathF.Sin(b)),field,1);}
var positions=Plan.Start.Select(s=>s.Point()).ToArray();
for(int phaseIndex=0;phaseIndex<Plan.Phases.Count;phaseIndex++)
{
var phase=Plan.Phases[phaseIndex];if(phase==null)continue;
float alpha=Math.Clamp(.94f-phaseIndex*.075f,.36f,.94f);
foreach(var route in phase.Routes??new())
{
if(route.Role<0 || route.Role>=5)continue;
var from=positions[route.Role];
foreach(var spot in route.Points??new())
{Arrow(painter,from,spot.Point(),new Color(.75f,.94f,.82f,alpha*.76f),1.2f);from=spot.Point();}
positions[route.Role]=from;
}
if(phase.Actor<0 || phase.Actor>=5 || phase.Receiver<0 || phase.Receiver>=5)continue;
if(phase.Action==0)
{
var target=phase.PassZone?.Point()??positions[phase.Receiver];
Arrow(painter,positions[phase.Actor],target,new Color(1,.86f,.52f,alpha),2.2f);
positions[phase.Receiver]=target;
}
else if(phase.Action==1)
Arrow(painter,positions[phase.Actor],new(105,31.5f+Math.Clamp(phase.Corner,0,2)*2.5f),new Color(1,.59f,.51f,alpha),2.3f);
}
for(int i=0;i<5;i++)
{
if(Plan.DefenderActive(i))Dot(painter,Plan.Defence[i].Point(),new Color(.91f,.56f,.40f),3);
Dot(painter,Plan.Start[i].Point(),new Color(.68f,.96f,.98f),3.4f);
}
}
}