A Sandbox UI panel for a soccer game's set-piece aiming. Hit-tests click positions against projected player positions, converts clicks inside the projected goal mouth into a normalized aim fraction through the Pick callback, and handles right-drag camera panning plus wheel zoom via MatchController.
using Sandbox;
using Sandbox.UI;
using Socer;
using System;
public sealed class SetPieceAimInput : Panel
{
public MatchController Game {get;set;}
public FootballWorldPanel World {get;set;}
public Action<float,float> Pick {get;set;}
bool panning;
Vector2 panLast;
Vector2 lastClick;
float lastClickAt=-10;
int lastPlayer=-1;
Vector2 Fraction(Vector2 pixel)=>new(pixel.x/Math.Max(1,Box.Rect.Width),pixel.y/Math.Max(1,Box.Rect.Height));
int FindPlayer(Vector2 pixel)
{
if(Game==null || World==null)return -1;
var candidates=new System.Collections.Generic.List<(int Id,float Distance)>();
foreach(var player in Game.Match.ActivePlayers)
{
var foot=World.Project(player.Position,.15f);var head=World.Project(player.Position,1.7f);
var a=new Vector2(foot.x*Box.Rect.Width,foot.y*Box.Rect.Height);
var b=new Vector2(head.x*Box.Rect.Width,head.y*Box.Rect.Height);
var segment=b-a;float length2=segment.x*segment.x+segment.y*segment.y;
var offset=pixel-a;float t=length2<1?0:(offset.x*segment.x+offset.y*segment.y)/length2;
if(t>1.12f || (t<-.2f && offset.Length>25))continue;
float d=(pixel-a-segment*Math.Clamp(t,0,1)).Length;
if(d<24)candidates.Add((player.Id,d));
}
if(candidates.Count==0)return -1;
candidates.Sort((a,b)=>a.Distance.CompareTo(b.Distance));
int index=0;
if(Time.Now-lastClickAt<1.1f && (pixel-lastClick).Length<20)
{
int previous=candidates.FindIndex(p=>p.Id==lastPlayer);
if(previous>=0)index=(previous+1)%candidates.Count;
}
lastClickAt=Time.Now;lastClick=pixel;lastPlayer=candidates[index].Id;
return lastPlayer;
}
public bool TrySelectPlayer(Vector2 pixel)
{
int player=FindPlayer(pixel);
if(player<0)return false;
Game.SelectPlayer(player);return true;
}
public bool InsideGoalMouth(Vector2 pixel)
{
if(Game==null || World==null)return false;
float x=Game.Match.HomeAttackGoal.X;
var a=World.Project(new Point(x,30.34f),0);
var b=World.Project(new Point(x,37.66f),0);
var c=World.Project(new Point(x,37.66f),2.44f);
var d=World.Project(new Point(x,30.34f),2.44f);
var p=Fraction(pixel);
return InTriangle(p,a,b,c) || InTriangle(p,a,c,d);
}
static float Cross(Vector2 a,Vector2 b)=>a.x*b.y-a.y*b.x;
static bool InTriangle(Vector2 point,Vector2 a,Vector2 b,Vector2 c)
{
float area=Cross(b-a,c-a);
if(!float.IsFinite(area) || Math.Abs(area)<.00000001f)return false;
float u=Cross(b-a,point-a)/area,v=Cross(point-a,c-a)/area;
return u>=-.0001f && v>=-.0001f && u+v<=1.0001f;
}
void MovePan(Vector2 pixel)
{
if(!panning || World==null || Game==null)return;
if((pixel-panLast).Length<.001f)return;
Game.PanBroadcastCamera(World.CameraPanDelta(Fraction(panLast),Fraction(pixel)));
panLast=pixel;
}
// The native pointer handler and editor interaction checks share exactly
// the same player hit-test, pixel mapping and aim callback.
public bool AimAt(Vector2 pixel)
{
if(Game==null || World==null || Pick==null)return false;
// A keeper or wall can cover the chosen point on screen. Inside the
// projected goal opening this click means aim; elsewhere players still
// receive the normal inspection/selection hit test.
if(!InsideGoalMouth(pixel) && TrySelectPlayer(pixel))return false;
Pick.Invoke(Math.Clamp(pixel.x/Math.Max(1,Box.Rect.Width),0,1),
Math.Clamp(pixel.y/Math.Max(1,Box.Rect.Height),0,1));
return true;
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
if(HasMouseCapture)SetMouseCapture(false);
if(e.MouseButton==MouseButtons.Right){panning=true;panLast=MousePosition;return;}
if(e.MouseButton!=MouseButtons.Left)return;
AimAt(MousePosition);
}
protected override void OnMouseUp(MousePanelEvent e)
{
base.OnMouseUp(e);
if(e.MouseButton!=MouseButtons.Right)return;
MovePan(MousePosition);panning=false;if(HasMouseCapture)SetMouseCapture(false);
}
public override void OnMouseWheel(Vector2 value)
{
base.OnMouseWheel(value);
Game?.ZoomBroadcastCamera(value.y);
}
public override void Tick(){base.Tick();if(panning)MovePan(MousePosition);}
}