StrategyCanvas.cs

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.

Rough Code
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));}
        }
    }
}