Set piece aiming panel for penalties and free kicks. Projects a ballistic arc of dots toward the goal, draws the reticle, and provides the charging power bar, power/shoot button, auto-release button, hotkey aiming and English/Russian help text. Click aiming solves a 2D Newton step against the match camera projection.
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@inherits Panel
<root class="localized-text">
<SetPieceAimInput Game="@Game" World="@World" Pick="@((Action<float,float>)Pick)" />
@for(int i=1;i<19;i++)
{var t=i/19f;var point=M.SetPieceSpot+(Target-M.SetPieceSpot)*t;float duration=(Target-M.SetPieceSpot).Length/(16+Power*20);float z=.12f*(1-t)+Height*t+4.905f*duration*duration*t*(1-t);
<div class="flight-dot" style="@Position(point,z)"></div>}
<div class="goal-reticle" style="@Position(Target,Height)"><label>+</label></div>
<div class="standard-bottom">
<div class="standard-name"><label>@(M.SetPiece==RestartKind.Penalty?Text("PENALTY","ПЕНАЛЬТИ"):Text("FREE KICK","ШТРАФНОЙ"))</label><label>@($"{M.Players[M.SetPieceTaker].Profile.Name} · {(Target-M.SetPieceSpot).Length:0} "+Text("m","м"))</label></div>
<label class="standard-help">@Text("Choose a point in goal. First click POWER to start charging, then SHOOT to lock power and kick. Right-drag pans the camera.","Выберите точку в воротах. Нажмите «СИЛА» для набора силы, затем «УДАР», чтобы зафиксировать её и пробить. Перетаскивание ПКМ двигает камеру.")</label>
<div class="power-track"><div class="power-fill" style="@FormattableString.Invariant($"width:{Power*100:0}%;")"></div><div class="power-risk"></div></div>
<div class="standard-actions"><button class="execute-setpiece" onclick=@Shoot>@(Charging?Text("SHOOT →","УДАР →"):Text("POWER →","СИЛА →"))</button><label>@($"{Power*100:0}%")</label><button class="auto-setpiece" onclick=@(()=>M.AutoReleaseSetPiece())>@Text("Let player decide","Игрок решит сам")</button></div>
<label class="standard-warning">@(M.ManualRestartWarned?Text("Referee: hurry up! Repeated delay risks a card. ","Судья: не затягивайте! Повторная задержка грозит карточкой. "):Text("Aim is a guide; accuracy depends on the taker. ","Прицел — ориентир; точность зависит от исполнителя. "))@(GamePreferences.Russian?$"Авторозыгрыш через {Math.Max(0,20-(int)M.ManualRestartWait)} с":$"Auto in {Math.Max(0,20-(int)M.ManualRestartWait)} s")</label>
</div>
</root>
@code {
public MatchController Game {get;set;}
public FootballWorldPanel World {get;set;}
MatchSimulation M=>Game.Match;
MatchSimulation controlMatch;
int sequence=-1;
float GoalY=34,Height=1.2f,chargeAt,chargePausedAt;
bool Charging,chargePaused;
static string Text(string en,string ru)=>GamePreferences.Russian?ru:en;
Point Target=>new Point(M.Direction(0)>0?105:0,GoalY);
float Power=>Charging?.15f+.85f*(1-MathF.Abs((((chargePaused?chargePausedAt:Time.Now)-chargeAt)/1.15f%2)-1)):.55f;
public override void Tick()
{
base.Tick();if(Game==null)return;
if(!ReferenceEquals(controlMatch,M) || sequence!=M.RestartSequence)
{controlMatch=M;sequence=M.RestartSequence;Charging=chargePaused=false;GoalY=34;Height=1.2f;}
// Tutorial kick input intentionally works while its match is paused.
// Only opening Settings freezes the power bar's presentation clock.
if(Charging && Game.GameSettingsOpen && !chargePaused){chargePaused=true;chargePausedAt=Time.Now;}
else if(chargePaused && !Game.GameSettingsOpen){chargeAt+=Time.Now-chargePausedAt;chargePaused=false;}
}
void Pick(float x,float y)
{
if(World==null)return;
// Intersect the click with the actual goal plane, using the same 3D camera as the match.
for(int i=0;i<9;i++)
{
var at=World.Project(Target,Height);var dy=World.Project(Target+new Point(0,.1f),Height)-at;
var dz=World.Project(Target,Height+.1f)-at;var error=new Vector2(x,y)-at;
float det=dy.x*dz.y-dy.y*dz.x;if(Math.Abs(det)<.0000001f)break;
GoalY=Math.Clamp(GoalY+Math.Clamp((error.x*dz.y-error.y*dz.x)/det*.1f,-2,2),29.6f,38.4f);
Height=Math.Clamp(Height+Math.Clamp((dy.x*error.y-dy.y*error.x)/det*.1f,-1,1),0,2.9f);
}
Game.ObserveMatchTutorialSetPieceAim(M);
}
void Shoot(){if(Game.GameSettingsOpen)return;if(!Charging){Charging=true;chargePaused=false;chargeAt=Time.Now;return;}if(Game.SubmitManualSetPieceShot(Target,Height,Power))Charging=chargePaused=false;}
public bool HandleHotkey(string id)
{
if(Game==null || Game.GameSettingsOpen || !M.ManualRestartWaiting)return false;
float horizontal=World!=null && World.Project(Target+new Point(0,.25f),Height).x<World.Project(Target,Height).x?-1:1;
switch(id)
{
case "standard.confirm":Shoot();return true;
case "standard.automatic":M.AutoReleaseSetPiece();return true;
case "standard.left":GoalY=Math.Clamp(GoalY-horizontal*.25f,29.6f,38.4f);break;
case "standard.right":GoalY=Math.Clamp(GoalY+horizontal*.25f,29.6f,38.4f);break;
case "standard.up":Height=Math.Clamp(Height+.15f,0,2.9f);break;
case "standard.down":Height=Math.Clamp(Height-.15f,0,2.9f);break;
default:return false;
}
Game.ObserveMatchTutorialSetPieceAim(M);return true;
}
string Position(Point p,float z){var q=World?.Project(p,z)??new Vector2(-10,-10);return FormattableString.Invariant($"left:{q.x*100:0.00}%;top:{q.y*100:0.00}%;");}
protected override int BuildHash()=>HashCode.Combine(Game?.Revision,GamePreferences.Revision);
}