A pair of UI panels for a soccer strategy game. The first draws chalk-style tactic overlays onto the pitch (draft routes, arrows, target rings, corner kick lines) projected from world coordinates; the second draws an 8-sector radial strategy wheel.
using Sandbox;
using Sandbox.UI;
using Socer;
using System;
// Deterministic chalk grain in field space. Drawing never consumes simulation randomness.
public sealed class StrategyCanvas : Panel
{
public MatchController Game {get;set;}
public FootballWorldPanel World {get;set;}
static float Grain(float x)=>MathF.Sin(x*12.9898f+78.233f);
public override void OnDraw(Painter p)
{
if(Game==null || World==null || Game.StrategyTesting || Game.StrategySeeking)return;
using var scope=p.Scope();p.Scale(ScaleToScreen);
float w=Box.Rect.Width*ScaleFromScreen,h=Box.Rect.Height*ScaleFromScreen;
Vector2 At(Point a){var v=World.Project(a,.07f);return new(v.x*w,v.y*h);}
void Chalk(Painter painter,Point a,Point b,Color color,float width,bool dotted=false)
{
var delta=b-a;float length=delta.Length;if(length<.02f)return;
var side=new Point(-delta.Unit.Y,delta.Unit.X);
int count=Math.Max(1,(int)MathF.Ceiling(length/.32f));
painter.Fill=Fill.None;
for(int layer=0;layer<3;layer++)
for(int i=0;i<count;i++)
{
if(dotted && (int)(i*.32f/1.2f)%2==1)continue;
float phase=a.X*3+a.Y*7+i*.61f+layer*31;
var start=a+delta*((float)i/count)+side*(Grain(phase)*.055f+(layer-1)*.028f);
var end=a+delta*((float)(i+1)/count)+side*(Grain(phase+.61f)*.055f+(layer-1)*.028f);
painter.Stroke=Stroke.Solid(color.WithAlpha((layer==1?.65f:.24f)*( .8f+.2f*Grain(phase+2))),width*(layer==1?.65f:.45f));
painter.Line(At(start),At(end));
}
}
void Head(Painter painter,Point a,Point b,Color color)
{
var d=(b-a).Unit;if(d.Length<.01f)return;var side=new Point(-d.Y,d.X);
Chalk(painter,b,b-d*1.35f+side*.7f,color,3);
Chalk(painter,b,b-d*1.35f-side*.7f,color,3);
}
void Ring(Painter painter,Point center,Color color)
{
for(int i=0;i<48;i++){float a=i*MathF.PI/24,b=(i+1)*MathF.PI/24;
Chalk(painter,center+new Point(MathF.Cos(a),MathF.Sin(a))*1.15f,center+new Point(MathF.Cos(b),MathF.Sin(b))*1.15f,color,2.5f);}
}
var f=Game.DraftPhase;
foreach(var r in f.Routes)
{
if(r.Points.Count==0)continue;
var start=Game.DraftPosition(r.Role);var previous=start;
var ink=r.Sprint?new Color(.88f,.95f,.66f):new Color(.88f,.95f,.91f);
foreach(var end in r.Points){previous=start;Chalk(p,start,end.Point(),ink,3.5f);start=end.Point();}
Head(p,previous,start,ink);Ring(p,start,ink);
}
var origin=Game.DraftPosition(f.Actor);
var passInk=new Color(.99f,.89f,.67f);
if(f.Action==0)
{
var target=f.PassZone?.Point()??Game.DraftPosition(f.Receiver);
Chalk(p,origin,target,passInk,3,true);Head(p,origin,target,passInk);
if(f.PassZone!=null)Ring(p,target,passInk);
if(f.Alternative>=0){var backup=Game.DraftPosition(f.Alternative);Chalk(p,origin,backup,new Color(.7f,.73f,.62f),1.5f,true);Head(p,origin,backup,new Color(.7f,.73f,.62f));}
}
if(f.Action==1){var target=new Point(105,31.5f+f.Corner*2.5f);var ink=new Color(1,.77f,.64f);Chalk(p,origin,target,ink,3.5f);Head(p,origin,target,ink);}
}
}
public sealed class StrategyWheelCanvas : Panel
{
public override void OnDraw(Painter p)
{
using var scope=p.Scope();p.Scale(ScaleToScreen);
float size=Math.Min(Box.Rect.Width,Box.Rect.Height)*ScaleFromScreen;var c=new Vector2(size/2,size/2);
Vector2 At(float angle,float r)=>c+new Vector2(MathF.Cos(angle),MathF.Sin(angle))*r;
p.Stroke=Stroke.None;
for(int sector=0;sector<8;sector++)
{
p.Fill=new Color(.18f+sector%2*.025f,.32f,.32f,.6f);
float start=(-112.5f+45*sector+1)*MathF.PI/180,end=start+43*MathF.PI/180;
for(int j=0;j<10;j++){float a=start+(end-start)*j/10,b=start+(end-start)*(j+1)/10;p.Quad(At(a,size*.18f),At(a,size*.485f),At(b,size*.485f),At(b,size*.18f));}
}
}
}