StrategyEditor.razor

The strategy editor panel for a soccer game. Draws the pitch overlay with player tags and arrows, and lets the player pick a role, choose pass, shot, carry, route, timing and defender options from a wheel or sidebar menu, then save, test or batch-run the drafted phases.

Rough Code
🐞 'F' is assigned once from G.DraftPhase in a field initializer, so after G.SelectStrategyPhase / AddStrategyPhase / DeleteStrategyPhase change the draft's current phase, F still points at the old StrategyPhase and every sidebar edit through Change/Route is applied to that old phase.
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@using System.Globalization
@inherits PanelComponent
<root class="@(MatchController.StrategyEditorAvailable && G?.StrategyLabOpen==true?"":"hidden")">
@if(MatchController.StrategyEditorAvailable && G?.StrategyLabOpen==true)
{
    @if(G.StrategyBrowserOpen)
    {
        <StrategyLibraryView Game="@G" />
    }
    else
    {
    <div class="lab-header">
        <div class="heading"><label class="eyebrow">SOCER / TACTICS LAB</label><div class="plan-heading"><TextEntry class="plan-name" Value:bind="@G.StrategyDraft.Name" /><label class="lab-success">@($"Success {G.StrategyDraft.EstimatedSuccessPercent()}%")</label></div></div>
        <div class="header-actions"><button class="lab-book" onclick=@(()=>G.ShowStrategyBrowser())>← All plays</button><button class="lab-save primary" onclick=@(()=>G.SaveStrategy())>Save</button><button class="lab-close" onclick=@(()=>G.CloseStrategyLab())>Close ↗</button></div>
    </div>
    <div class="editor-main"><div class="board-frame" @ref="Board">
        <FootballWorldPanel class="lab-world" Game="@G" @ref="World" />
        <StrategyCanvas Game="@G" World="@World" />
        <StrategyFieldInput class="field-input" Game="@G" World="@World" Press="@((Action<Vector2>)PointerDown)" Drag="@((Action<Vector2>)PointerDrag)" Release="@((Action<Vector2>)PointerUp)" />
        <div class="camera-tools"><label>Right drag · pan / wheel · zoom</label><button class="lab-zoom-out" onclick=@(()=>G.ZoomStrategyCamera(-2))>−</button><button class="lab-zoom-in" onclick=@(()=>G.ZoomStrategyCamera(2))>+</button><button class="lab-focus" onclick=@(()=>G.FocusStrategyCamera())>Focus ball</button></div>
        @for(int i=0;i<10;i++)
        {
            int role=i;var actor=G.Match.Players[i<5?6+i:12+i-5];
            @if(i<5 || G.StrategyDraft.DefenderActive(i-5)){<div class="player-tag @(i==G.StrategyRole?"picked":"") @(i>=5?"opponent":"")" style="@Position(actor.Position,2.05f)"><label>@(i<5?((char)('A'+i)).ToString():$"#{actor.Number}")</label></div>}
        }
        <div class="scene-info"><label class="eyebrow">@(G.StrategySeeking?"REHEARSAL · NO INTERCEPTIONS":G.StrategyTesting?"TRIAL":"BUILD A PLAY")</label><label class="phase-title">@($"Phase {G.StrategyPhaseIndex+1:00}")</label><label>@(G.StrategySeeking || G.StrategyTesting?UiEnglish.Text(G.Match.Strategy?.Status,"Play in progress"):ActionCaption)</label></div>
        @if(!G.StrategyTesting && !G.StrategySeeking)
        {
            <div class="field-hint"><label>@Hint</label>@if(G.StrategyTool!=0){<button class="lab-finish-draw" onclick=@FinishDrawing>Done ✓</button><button class="lab-cancel-draw" onclick=@CancelDrawing>Cancel ✕</button>}</div>
            @if(SelectedMark!=0){<div class="arrow-toolbar" style="@MarkPosition"><label>@(SelectedMark==1?"Run":SelectedMark==2?"Pass":SelectedMark==3?"Shot":"Backup pass")</label><button class="lab-delete-arrow" onclick=@DeleteMark>Delete ×</button></div>}
        }
        @if(Wheel && !G.StrategySeeking && !G.StrategyTesting)
        {
            <div class="wheel" style="@WheelPosition">
                <StrategyWheelCanvas />
                <div class="wheel-center"><label>@(G.StrategyRole<5?$"PLAYER {(char)('A'+G.StrategyRole)}":"DEFENDER")</label><button class="wheel-back" onclick=@(()=>{if(Menu==0)Wheel=false;else Menu=0;})>@(Menu==0?"Close":"← Back")</button></div>
                @for(int i=0;i<8;i++){int item=i;<button class="wheel-item wheel-action-@i" style="@WheelItem(i)" onclick=@(()=>Choose(item))><img class="wheel-icon" src="@WheelIcon(i)" /><label>@MenuLabels[i]</label></button>}
            </div>
        }
    </div>
    <div class="editor-sidebar">
        <label class="eyebrow">PHASE @(G.StrategyPhaseIndex+1) / @G.StrategyDraft.Phases.Count</label>
        <label class="sidebar-title">@(G.StrategyRole<5?$"Player {(char)('A'+G.StrategyRole)}":$"Defender #{G.StrategyRole-4}")</label>
        @if(G.StrategyRole<5)
        {
            <label class="sidebar-copy">Choose an action, then draw its direction on the pitch.</label>
            <div class="sidebar-actions"><button class="lab-action-pass @(F.Action==0?"active":"")" onclick=@BeginPass><img class="tactic-icon" src="/ui-icons/ui-line-strategy-pass.svg" />Pass</button><button class="lab-action-shot @(F.Action==1?"active":"")" onclick=@BeginShot><img class="tactic-icon" src="/ui-icons/ui-line-strategy-power-shot.svg" />Shot</button><button class="lab-action-carry @(F.Action==2?"active":"")" onclick=@BeginCarry><img class="tactic-icon" src="/ui-icons/ui-line-strategy-sprint.svg" />Carry</button></div>
            @if(F.Action==0)
            {
                <label class="eyebrow">PASS TYPE</label><div class="sidebar-grid">@for(int i=0;i<4;i++){int kind=i;<button class="@(F.Delivery==i?"active":"")" onclick=@(()=>Change(()=>F.Delivery=kind))><img class="tactic-icon" src="@($"/ui-icons/ui-line-strategy-{new[]{"pass","through","cross","cutback"}[i]}.svg")" />@PassKinds[i]</button>}</div>
                <button class="lab-pass-space" onclick=@BeginPass><img class="tactic-icon" src="/ui-icons/ui-line-strategy-through.svg" />Draw pass to open space</button><button class="lab-backup-pass" onclick=@BeginBackup><img class="tactic-icon" src="/ui-icons/ui-line-strategy-backup.svg" />Choose backup receiver</button>
            }
            @if(F.Action==1)
            {
                <label class="eyebrow">SHOT STYLE</label><div class="sidebar-grid">@for(int i=0;i<4;i++){int kind=i;<button class="@(F.Shot==i?"active":"")" onclick=@(()=>Change(()=>F.Shot=kind))><img class="tactic-icon" src="@($"/ui-icons/ui-line-strategy-{new[]{"low-shot","power-shot","curled-shot","chip-shot"}[i]}.svg")" />@ShotKinds[i]</button>}</div>
                <div class="sidebar-grid"><button class="@(F.FirstTime?"active":"")" onclick=@(()=>Change(()=>{F.FirstTime=!F.FirstTime;if(F.FirstTime)F.OnRun=false;}))>First time</button><button class="@(F.OnRun?"active":"")" onclick=@(()=>Change(()=>{F.OnRun=!F.OnRun;if(F.OnRun)F.FirstTime=false;}))>On the run</button></div>
                <button class="lab-aim-shot" onclick=@BeginAim><img class="tactic-icon" src="/ui-icons/ui-line-strategy-position.svg" />Set goal corner</button>
            }
            <label class="eyebrow">MOVEMENT</label><div class="sidebar-grid"><button class="lab-draw-run" onclick=@(()=>BeginRoute(false))><img class="tactic-icon" src="/ui-icons/ui-line-strategy-position.svg" />Draw route</button><button class="lab-draw-sprint" onclick=@(()=>BeginRoute(true))><img class="tactic-icon" src="/ui-icons/ui-line-strategy-sprint.svg" />Sprint route</button></div>
            <button class="lab-remove-route" onclick=@RemoveSelectedRoute><img class="tactic-icon" src="/ui-icons/ui-line-strategy-clear.svg" />Remove player route</button><button class="lab-clear-action" onclick=@ClearAction><img class="tactic-icon" src="/ui-icons/ui-line-strategy-clear.svg" />Remove phase action</button>
        }
        else
        {
            <label class="sidebar-copy">Drag this defender in phase 1. Remove or restore it in the defender list below.</label>
        }
        <div class="sidebar-spacer"></div>
        <label class="eyebrow">TRAINING OPPOSITION</label><button class="lab-dummy-mode" onclick=@(()=>G.ToggleStrategyDummyMode())>@(G.StrategyDummyMode?"Walking dummies · switch to active":"Active defenders · switch to dummies")</button>
        <button class="lab-defence" onclick=@(()=>{G.StrategyDefence=(G.StrategyDefence+1)%3;G.SelectStrategyPhase(0);})>@($"Defence: {UiEnglish.Text(Defences[G.StrategyDefence],"Balanced")} ↻")</button>
        <div class="defender-list">@for(int i=0;i<5;i++){int defender=i;<button class="@(G.StrategyDraft.DefenderActive(i)?"":"inactive")" onclick=@(()=>G.ToggleStrategyDefender(defender))>@($"#{i+1} · {(G.StrategyDraft.DefenderActive(i)?"Remove":"Restore")}")</button>}</div>
        <button class="lab-batch" onclick=@(()=>G.StrategyBatch())>Run 20 trials</button>
        <button class="lab-delete-phase" disabled="@(G.StrategyDraft.Phases.Count<=1)" onclick=@(()=>G.DeleteStrategyPhase())>Delete this phase</button>
        @if(G.HasStrategyPhasesAfterShot){<button class="lab-remove-after-shot" disabled="@(G.StrategySeeking || G.StrategyTesting)" onclick=@(()=>G.RemoveStrategyPhasesAfterShot())>Remove phases after shot</button>}
    </div></div>
    <div class="timeline">
        <button class="lab-undo" onclick=@(()=>{Wheel=false;G.UndoStrategy();})>↶</button>
        <div class="phase-strip">@for(int i=0;i<G.StrategyDraft.Phases.Count;i++){int phase=i;<button class="lab-phase-@i @(G.StrategyPhaseIndex==i?"active":"")" onclick=@(()=>{Wheel=false;G.SelectStrategyPhase(phase);})><label>@($"{i+1:00}")</label><label class="phase-type">@PhaseLabel(G.StrategyDraft.Phases[i])</label></button>}</div>
        <button class="lab-next @(G.StrategyPhaseIndex>=G.StrategyDraft.Phases.Count-1 || G.StrategyTesting || G.DraftPhase.Action==1?"unavailable":"")" disabled="@(G.StrategyPhaseIndex>=G.StrategyDraft.Phases.Count-1 || G.StrategyTesting || G.DraftPhase.Action==1)" onclick=@(()=>{Wheel=false;G.AdvanceStrategyPhase();})>Next phase →</button>
        <button class="lab-add-phase primary @(G.StrategySeeking || G.StrategyTesting || G.StrategyDraft.Phases.Count>=16 || G.StrategyDraft.Phases.Any(p=>p.Action==1)?"unavailable":"")" disabled="@(G.StrategySeeking || G.StrategyTesting || G.StrategyDraft.Phases.Count>=16 || G.StrategyDraft.Phases.Any(p=>p.Action==1))" onclick=@(()=>{Wheel=false;G.AddStrategyPhase();})>+ Add phase</button>
        <button class="lab-run" onclick=@(()=>{Wheel=false;G.RunStrategyTrial();})>▶ Test</button>
        @if(G.StrategyTesting || G.StrategySeeking){<button class="lab-pause" onclick=@(()=>G.ToggleStrategyPlayback())>@(G.Paused?"▶":"Ⅱ")</button>}
        <button class="lab-reset" onclick=@(()=>{Wheel=false;G.SelectStrategyPhase(0);})>To start</button>
    </div>
    <div class="lab-status"><label>@UiEnglish.Text(G.StrategyNotice,"Plan updated")</label><label>@UiEnglish.Text(G.StrategyReport,"Trial complete")</label></div>
    }
}
</root>
@code {
    protected override void OnTreeBuilt(){base.OnTreeBuilt();UiRussian.Apply(Panel);}
    MatchController cachedGame;
    MatchController G
    {
        get
        {
            if(cachedGame?.GameObject?.IsValid!=true || cachedGame.Scene!=Scene)
                cachedGame=Scene.GetAllComponents<MatchController>().FirstOrDefault();
            return cachedGame;
        }
    }
    StrategyPhase F=>G.DraftPhase;
    Panel Board {get;set;}
    FootballWorldPanel World {get;set;}
    bool Wheel;
    int SelectedMark,SelectedMarkRole,DragRole=-1;
    bool DidDrag;
    Vector2 DownPoint,MarkPoint;
    int Menu;
    float WheelX=.5f,WheelY=.5f;
    static readonly string[] Defences={"Low block","Balanced","High press"};
    static readonly string[] PassKinds={"Ground","Through","Lofted","Driven"};
    static readonly string[] ShotKinds={"Low","Power","Curled","Chip"};
    string WheelIcon(int index)
    {
        var icon=MenuLabels[index] switch {
            "Pass" or "Ground" or "Give ball"=>"pass",
            "Through ball"=>"through",
            "Cross"=>"cross",
            "Cutback"=>"cutback",
            "Backup pass"=>"backup",
            "Wait for run" or "After control" or "After 0.5 s" or "After 1 s" or "After 2 s" or "Wait 4 s" or "Wait 6 s" or "Wait 10 s"=>"wait",
            "Pass now" or "Now" or "First time"=>"instant",
            "Low" or "Shot"=>"low-shot",
            "Power"=>"power-shot",
            "Curled"=>"curled-shot",
            "Chip"=>"chip-shot",
            "Overlap"=>"overlap",
            "Sprint" or "Dribble"=>"sprint",
            "Position" or "Position / ball" or "Move" or "Aim"=>"position",
            "Timing"=>"wait",
            "Back" or "Close"=>"back",
            _=>"clear"
        };
        return $"/ui-icons/ui-line-strategy-{icon}.svg";
    }
    string[] MenuLabels=>G.StrategyRole>=5?new[]{"—","—","—","—","—","Position","—","Close"}:Menu switch {
        1=>new[]{"Ground","Through ball","Cross","Cutback","Backup pass","Wait for run","Pass now","Back"},
        2=>new[]{"Ground","Power","Curled","Chip","First time","After control","Aim","Back"},
        4=>new[]{"Move","Give ball","Back","—","—","—","—","Back"},
        3=>new[]{"Now","After 0.5 s","After 1 s","After 2 s","Wait 4 s","Wait 6 s","Wait 10 s","Back"},
        _=>new[]{"Pass","Overlap","Sprint","Shot","Dribble","Position / ball","Timing","Clear"}};
    string Hint=>G.StrategySeekFailed?"Play failed. Return to the start and change the actions.":G.StrategyTool switch {
        1=>"Click the pitch to move this player. You can also drag players directly in phase 1.",
        2=>"Click the pitch to draw a route. Each point adds a turn. Then press Done.",
        3=>"Click open space to aim the pass arrow; the closest teammate will chase it.",
        4=>"Choose a teammate for a backup pass if the main lane is blocked.",
        5=>"Click the goal to set the shot direction.",
        _=>G.StrategyPhaseIndex==0?"Drag players and defenders to set the starting positions. Click an arrow to edit or delete it.":"Select a player or arrow, then edit the action in the right panel."};
    string ActionCaption=>$"{(char)('A'+F.Actor)} · {PhaseLabel(F)}";
    static string PhaseLabel(StrategyPhase p)=>p.Action==1?(p.FirstTime?"First-time shot":p.OnRun?"Shot on run":"Shot"):p.Action==2?"Carry":p.PassZone!=null?"Pass to space":$"Pass → {(char)('A'+p.Receiver)}";
    string Position(Point p,float z=0){var q=World?.Project(p,z)??new Vector2(-1,-1);return FormattableString.Invariant($"left: {q.x*100:0.###}%; top: {q.y*100:0.###}%;");}
    string WheelPosition=>FormattableString.Invariant($"left: {WheelX*100:0.###}%; top: {WheelY*100:0.###}%;");
    string MarkPosition=>FormattableString.Invariant($"left: {Math.Clamp(MarkPoint.x,0.08f,.92f)*100:0.###}%; top: {Math.Clamp(MarkPoint.y,0.12f,.88f)*100:0.###}%;");
    string WheelItem(int i){float a=(-90+i*45)*MathF.PI/180;return FormattableString.Invariant($"left: {50+MathF.Cos(a)*34:0.###}%; top: {50+MathF.Sin(a)*34:0.###}%;");}
    public void PointerDown(Vector2 f)
    {
        if(Board==null || World==null || G.StrategySeeking || G.StrategyTesting)return;
        DownPoint=f;DidDrag=false;DragRole=-1;Wheel=false;
        if(G.StrategyPhaseIndex==0 && G.StrategyTool==0)
        {
            int nearest=Enumerable.Range(0,10).Where(i=>i<5 || G.StrategyDraft.DefenderActive(i-5)).OrderBy(i=>ScreenDistance(i,f)).First();
            if(ScreenDistance(nearest,f)<34){DragRole=nearest;G.StrategyRole=nearest;G.SelectedPlayer=nearest<5?6+nearest:12+nearest-5;SelectedMark=0;return;}
        }
        PickAt(f);
    }
    public void PointerDrag(Vector2 f)
    {
        if(DragRole<0 || Board==null || G.StrategyPhaseIndex!=0)return;
        float dx=(f.x-DownPoint.x)*Board.Box.Rect.Width,dy=(f.y-DownPoint.y)*Board.Box.Rect.Height;
        if(!DidDrag && dx*dx+dy*dy<36)return;
        if(!DidDrag){G.BeginStrategyDrawing();G.RememberStrategy();DidDrag=true;SelectedMark=0;}
        G.MoveStrategyStart(DragRole,World.PickField(new Vector2(Math.Clamp(f.x,0,1),Math.Clamp(f.y,0,1))));
    }
    public void PointerUp(Vector2 f)
    {
        if(DragRole<0)return;
        if(DidDrag){G.CommitStrategyDrawing();Wheel=false;}
        else PickAt(f);
        DragRole=-1;DidDrag=false;
    }
    void BeginPass()
    {
        if(G.StrategyRole>=5)return;
        G.BeginStrategyDrawing();Change(()=>{F.Actor=G.StrategyRole;F.Action=0;if(F.Receiver==F.Actor)F.Receiver=(F.Actor+1)%5;F.PassZone=null;});
        G.StrategyTool=3;SelectedMark=0;Wheel=false;
    }
    void BeginBackup(){G.BeginStrategyDrawing();G.StrategyTool=4;SelectedMark=0;}
    void BeginShot(){if(G.StrategyRole>=5)return;Change(()=>{F.Actor=G.StrategyRole;F.Action=1;F.PassZone=null;});G.StrategyTool=0;SelectedMark=0;}
    void BeginAim(){G.BeginStrategyDrawing();G.StrategyTool=5;SelectedMark=0;}
    void BeginCarry(){if(G.StrategyRole>=5)return;Change(()=>{F.Actor=G.StrategyRole;F.Action=2;F.PassZone=null;});BeginRoute(false);}
    void BeginRoute(bool sprint){if(G.StrategyRole>=5)return;G.BeginStrategyDrawing();Route(r=>r.Sprint=sprint);G.StrategyTool=2;SelectedMark=0;}
    void RemoveSelectedRoute(){if(G.StrategyRole>=5)return;Change(()=>F.Routes.RemoveAll(r=>r.Role==G.StrategyRole));SelectedMark=0;}
    void ClearAction(){Change(()=>{F.Action=2;F.PassZone=null;F.FirstTime=false;F.OnRun=false;});SelectedMark=0;}
    void DeleteMark()
    {
        int kind=SelectedMark,role=SelectedMarkRole;
        if(kind==0)return;
        Change(()=>{if(kind==1)F.Routes.RemoveAll(r=>r.Role==role);else if(kind==4)F.Alternative=-1;else{F.Action=2;F.PassZone=null;F.FirstTime=false;F.OnRun=false;}});
        SelectedMark=0;
    }
    float SegmentDistance(Vector2 mouse,Point a,Point b,out Vector2 middle)
    {
        var pa=World.Project(a,.07f);var pb=World.Project(b,.07f);
        middle=new Vector2(pa.x+(pb.x-pa.x)*.65f,pa.y+(pb.y-pa.y)*.65f);
        var start=new Vector2(pa.x*Board.Box.Rect.Width,pa.y*Board.Box.Rect.Height);
        var end=new Vector2(pb.x*Board.Box.Rect.Width,pb.y*Board.Box.Rect.Height);
        var at=new Vector2(mouse.x*Board.Box.Rect.Width,mouse.y*Board.Box.Rect.Height);
        var delta=end-start;float length=delta.Length;
        float t=length<.01f?0:Math.Clamp(((at.x-start.x)*delta.x+(at.y-start.y)*delta.y)/(length*length),0,1);
        return (at-(start+delta*t)).Length;
    }
    bool SelectArrow(Vector2 f)
    {
        float best=18;int kind=0,role=0;Vector2 place=default;
        void Consider(int candidate,int selectedRole,Point a,Point b)
        {float distance=SegmentDistance(f,a,b,out var mid);if(distance>=best)return;best=distance;kind=candidate;role=selectedRole;place=mid;}
        foreach(var route in F.Routes)
        {
            Point from=G.DraftPosition(route.Role);
            foreach(var spot in route.Points){Consider(1,route.Role,from,spot.Point());from=spot.Point();}
        }
        Point origin=G.DraftPosition(F.Actor);
        if(F.Action==0)
        {
            Consider(2,F.Actor,origin,F.PassZone?.Point()??G.DraftPosition(F.Receiver));
            if(F.Alternative>=0)Consider(4,F.Actor,origin,G.DraftPosition(F.Alternative));
        }
        if(F.Action==1)Consider(3,F.Actor,origin,new Point(105,31.5f+F.Corner*2.5f));
        if(kind==0){SelectedMark=0;return false;}
        SelectedMark=kind;SelectedMarkRole=role;MarkPoint=place;G.StrategyRole=role;Wheel=false;return true;
    }
    public void PickAt(Vector2 f)
    {
        if(Board==null || World==null || G.StrategySeeking || G.StrategyTesting)return;
        if(Wheel){Wheel=false;return;}
        int nearest=Enumerable.Range(0,10).Where(i=>i<5 || G.StrategyDraft.DefenderActive(i-5)).OrderBy(i=>ScreenDistance(i,f)).First();
        bool hit=ScreenDistance(nearest,f)<34;
        if(G.StrategyTool==0)
        {
            if(hit){G.StrategyRole=nearest;G.SelectedPlayer=nearest<5?6+nearest:12+nearest-5;SelectedMark=0;return;}
            SelectArrow(f);return;
        }
        if(G.StrategyTool==3 || G.StrategyTool==4)
        {
            if(hit && nearest<5 && nearest!=F.Actor){G.StrategyFieldClick(G.DraftPosition(nearest));FinishDrawing();return;}
            if(G.StrategyTool==4)return;
            var zone=World.PickField(f);
            Change(()=>{F.PassZone=new(zone.X,zone.Y);F.Receiver=Enumerable.Range(0,5).Where(i=>i!=F.Actor).OrderBy(i=>(G.DraftPosition(i)-zone).Length).First();});
            FinishDrawing();return;
        }
        var point=World.PickField(f);
        if(G.StrategyTool==5)
        {Change(()=>F.Corner=point.Y<32.8f?0:point.Y>35.2f?2:1);FinishDrawing();return;}
        G.StrategyFieldClick(point);
        if(G.StrategyTool==1)FinishDrawing();
    }
    public void PickGround(float x,float y)=>PickAt(World.Project(new Point(x,y)));
    public void DragPlayer(int role,float x,float y)
    {
        role=Math.Clamp(role,0,9);
        var from=World.Project(G.DraftPosition(role),1);
        var to=World.Project(new Point(x,y));
        PointerDown(from);
        PointerDrag(to);
        PointerUp(to);
    }
    public object DebugView()=>new{View=World?.DebugView(),Board=Board?.Box.Rect.ToString(),A=World?.Project(G.DraftPosition(0)),C=World?.Project(G.DraftPosition(2))};
    public void PickPlayer(int role)=>PickAt(World.Project(G.DraftPosition(role),1));
    float ScreenDistance(int role,Vector2 mouse)
    {
        var foot=World.Project(G.DraftPosition(role));var body=World.Project(G.DraftPosition(role),1);
        return Math.Min(new Vector2((foot.x-mouse.x)*Board.Box.Rect.Width,(foot.y-mouse.y)*Board.Box.Rect.Height).Length,new Vector2((body.x-mouse.x)*Board.Box.Rect.Width,(body.y-mouse.y)*Board.Box.Rect.Height).Length);
    }
    void FinishDrawing(){G.CommitStrategyDrawing();Wheel=false;}
    void CancelDrawing(){G.CancelStrategyDrawing();Wheel=false;}
    void Change(Action change){if(G.StrategySeeking || G.StrategyTesting || G.StrategySeekFailed)return;G.RememberStrategy();change();if(G.StrategyPhaseIndex==0){G.Match.Owner=6+F.Actor;G.Match.Ball=G.DraftPosition(F.Actor);G.Match.BallVelocity=new();}}
    void Route(Action<StrategyRoute> change){Change(()=>{var r=F.Routes.FirstOrDefault(r=>r.Role==G.StrategyRole);if(r==null){r=new(){Role=G.StrategyRole};F.Routes.Add(r);}change(r);});}
    void Choose(int item)
    {
        if(G.StrategySeeking || G.StrategyTesting || G.StrategySeekFailed){Wheel=false;return;}
        if(G.StrategyRole>=5){if(item==5 && G.StrategyPhaseIndex==0){G.BeginStrategyDrawing();G.StrategyTool=1;Wheel=false;}else Wheel=false;return;}
        if(Menu==4)
        {
            if(item==0 && G.StrategyPhaseIndex==0){G.BeginStrategyDrawing();G.StrategyTool=1;Wheel=false;}
            else if(item==1 && G.StrategyPhaseIndex==0){Change(()=>{F.Actor=G.StrategyRole;if(F.Receiver==F.Actor)F.Receiver=(F.Actor+1)%5;if(F.Alternative==F.Actor)F.Alternative=-1;});Wheel=false;}
            else Menu=0;return;
        }
        if(Menu==1)
        {
            if(item==7){Menu=0;return;}
            if(item==5 || item==6){Change(()=>F.WaitForRun=item==5);return;}
            G.BeginStrategyDrawing();Change(()=>{F.Actor=G.StrategyRole;F.Action=0;if(F.Receiver==F.Actor)F.Receiver=(F.Actor+1)%5;if(F.Alternative==F.Actor)F.Alternative=-1;if(item<4)F.Delivery=item;});
            G.StrategyTool=item==4?4:3;Wheel=false;return;
        }
        if(Menu==2)
        {
            if(item==7){Menu=0;return;}
            G.BeginStrategyDrawing();Change(()=>{F.Actor=G.StrategyRole;F.Action=1;if(item<4)F.Shot=item;if(item==4)F.FirstTime=true;if(item==5)F.FirstTime=false;});
            G.StrategyTool=5;Wheel=false;return;
        }
        if(Menu==3)
        {
            if(item==7){Menu=0;return;}
            if(item<4){float delay=new[]{0f,.5f,1f,2f}[item];Route(r=>r.Delay=delay);if(F.Actor==G.StrategyRole)Change(()=>F.Wait=delay);}
            else Change(()=>F.Timeout=new[]{4f,6f,10f}[item-4]);Wheel=false;return;
        }
        if(item==0){Menu=1;return;}if(item==3){Menu=2;return;}if(item==6){Menu=3;return;}
        if(item==5){Menu=4;return;}
        if(item==7){Change(()=>{F.Routes.RemoveAll(r=>r.Role==G.StrategyRole);if(F.Actor==G.StrategyRole)F.Action=2;});FinishDrawing();return;}
        G.BeginStrategyDrawing();Route(r=>r.Sprint=item==2);
        if(item==4)Change(()=>{F.Actor=G.StrategyRole;F.Action=2;});
        G.StrategyTool=2;Wheel=false;
    }
    protected override int BuildHash()=>G?.StrategyLabOpen==true?G.Revision:0;
}