UI panel for the soccer career screen. Builds squad skill connection links from the career roster, positions markers over the player layout, and shows a popup when a link is hovered or pinned.
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@inherits Panel
<root class="squad-skill-links localized-text @(Interacting?"interacting":"")">
<SquadSkillLinkCanvas Routes="@Routes" GeometrySize="@(new Point(Box.Rect.Width*ScaleFromScreen,Box.Rect.Height*ScaleFromScreen))" FocusedSlot="@FocusSlot" FocusedLink="@(Active?.Key??-1)" />
@foreach(var link in Links)
{
var key=link.Key;
<button class="squad-link-marker squad-link-key-@key @(link.Golden?"golden":"") @(Active?.Key==key?"chosen":"") @(Dim(link)?"dimmed":"")" style="@MarkerStyle(link)" onmouseover=@(()=>Show(key)) onmouseout=@Leave onclick=@(()=>Pin(key))>
<SkillBadgeView Skill="@link.Pairs[0].First.Skill" Grade="@link.Pairs[0].First.Grade" CssClass="squad-link-badge" />
<SkillBadgeView Skill="@link.Pairs[0].Second.Skill" Grade="@link.Pairs[0].Second.Grade" CssClass="squad-link-badge" />
</button>
}
</root>
@code {
[Parameter] public ClubCareer Career {get;set;}
[Parameter] public Point[] Layout {get;set;}
[Parameter] public bool Interacting {get;set;}
SquadSkillConnection[] cached=Array.Empty<SquadSkillConnection>();
SquadSkillRoute[] routes=Array.Empty<SquadSkillRoute>();
Point[] centers=Array.Empty<Point>();
readonly SquadSkillConnection[] connectionByKey=new SquadSkillConnection[121];
readonly string[] markerStyles=new string[121];
readonly PlayerCard[] rosterCards=new PlayerCard[11];
static readonly string[] slotClasses={"squad-position-0","squad-position-1","squad-position-2","squad-position-3","squad-position-4","squad-position-5","squad-position-6","squad-position-7","squad-position-8","squad-position-9","squad-position-10"};
int stamp=int.MinValue,Hover=-1,Pinned=-1,FocusSlot=-1;
int geometryStamp=int.MinValue;
float LeaveAt,lastRosterPoll=float.MinValue,lastLayoutPoll=float.MinValue;
int layoutStamp,rosterCount=-1,markerKey=-1;
SquadSkillLinkPopup popup;
ClubCareer cachedCareer,rosterCareer,polledCareer;
bool lookupReady;
object rosterCollection;
Point[] polledLayout;
float polledScale;
Panel activeMarker;
SquadSkillConnection[] Links {get{RefreshLinks();return cached;}}
SquadSkillRoute[] Routes {get{RefreshLinks();RefreshGeometry();return routes;}}
SquadSkillConnection Active
{
get{if(Interacting)return null;RefreshLinks();int key=Pinned>=0?Pinned:Hover;return key>=0 && key<connectionByKey.Length?connectionByKey[key]:null;}
}
int RosterStamp()
{
if(Career==null)return 0;
var cards=Career.Cards;
bool resolve=!ReferenceEquals(rosterCareer,Career) || !ReferenceEquals(rosterCollection,cards) || rosterCount!=cards.Count;
rosterCareer=Career;rosterCollection=cards;rosterCount=cards.Count;
var hash=new HashCode();hash.Add(Career.FormationIndex);
for(int slot=0;slot<11;slot++)
{
int id=slot<Career.Lineup.Length?Career.Lineup[slot]:-1;
var card=rosterCards[slot];
if(resolve || (card?.Id??-1)!=id)
{
card=null;
for(int index=0;index<cards.Count;index++)if(cards[index].Id==id){card=cards[index];break;}
rosterCards[slot]=card;
}
hash.Add(card?.Id);hash.Add(card?.CardKey);hash.Add(card?.Name);
if(card?.SkillLoadout==null)continue;
for(int index=0;index<card.SkillLoadout.Count;index++)
{var skill=card.SkillLoadout[index];hash.Add(skill?.Skill);hash.Add(skill?.Grade);}
}
return hash.ToHashCode();
}
void RefreshLinks()
{
float now=Time.Now;if(lastRosterPoll==now && ReferenceEquals(Career,polledCareer))return;
lastRosterPoll=now;polledCareer=Career;
int next=RosterStamp();if(next==stamp && ReferenceEquals(Career,cachedCareer) && lookupReady)return;
stamp=next;cachedCareer=Career;cached=SquadSkillConnections.Build(Career);Hover=Pinned=-1;geometryStamp=int.MinValue;
Array.Clear(connectionByKey,0,connectionByKey.Length);
foreach(var connection in cached)connectionByKey[connection.Key]=connection;
lookupReady=true;
activeMarker=null;markerKey=-1;
}
int LayoutStamp()
{
float now=Time.Now,scale=ScaleFromScreen;
if(lastLayoutPoll==now && ReferenceEquals(Layout,polledLayout) && scale==polledScale)return layoutStamp;
lastLayoutPoll=now;polledLayout=Layout;polledScale=scale;
var hash=new HashCode();hash.Add(scale);
if(Layout!=null)for(int index=0;index<Layout.Length;index++){hash.Add(Layout[index].X);hash.Add(Layout[index].Y);}
layoutStamp=hash.ToHashCode();return layoutStamp;
}
void RefreshGeometry()
{
if(Layout?.Length!=11)return;
float w=MathF.Round(Box.Rect.Width*ScaleFromScreen*4)/4,h=MathF.Round(Box.Rect.Height*ScaleFromScreen*4)/4;
if(w<=0 || h<=0)return;
int next=HashCode.Combine(stamp,w,h,LayoutStamp());if(next==geometryStamp && routes.Length==cached.Length && centers.Length==11)return;
geometryStamp=next;
centers=Layout.Select(p=>new Point(p.X*w/100,p.Y*h/100)).ToArray();
routes=SquadSkillLinkGeometry.Build(cached,centers,w,h);
Array.Clear(markerStyles,0,markerStyles.Length);
foreach(var route in routes)
markerStyles[route.Connection.Key]=FormattableString.Invariant($"display:flex;left:{route.Marker.X/w*100}%;top:{route.Marker.Y/h*100}%;");
}
string MarkerStyle(SquadSkillConnection link){RefreshGeometry();return markerStyles[link.Key]??"display:none;";}
bool Dim(SquadSkillConnection link)=>Active is { } active?active.Key!=link.Key:FocusSlot>=0 && link.FirstSlot!=FocusSlot && link.SecondSlot!=FocusSlot;
void Show(int key){Hover=key;LeaveAt=0;}
void Leave(){LeaveAt=Time.Now+.16f;}
void Pin(int key){Pinned=Pinned==key?-1:key;Hover=key;}
void Clear(){Pinned=Hover=-1;LeaveAt=0;}
public override void Tick()
{
base.Tick();
if(Interacting || !IsVisible){Clear();FocusSlot=-1;if(popup!=null)popup.Connection=null;return;}
if(LeaveAt>0 && Time.Now>=LeaveAt && popup?.TipHovered!=true){Hover=-1;LeaveAt=0;}
var active=Active;
if(active!=null && IsVisible)
{
var root=FindRootPanel();
if(root!=null && (popup==null || popup.IsDeleting)){popup=root.AddChild<SquadSkillLinkPopup>();popup.OnClose=Clear;}
if(markerKey!=active.Key || activeMarker==null || activeMarker.IsDeleting)
{
markerKey=active.Key;activeMarker=null;string cssClass="squad-link-key-"+active.Key;
foreach(var child in Children)if(child.HasClass(cssClass)){activeMarker=child;break;}
}
var marker=activeMarker;
if(popup!=null && marker!=null)
{
var a=marker.PanelPositionToScreenPosition(Vector2.Zero);var b=marker.PanelPositionToScreenPosition(new Vector2(marker.Box.Rect.Width,marker.Box.Rect.Height));
popup.Connection=active;popup.Career=Career;popup.Pinned=Pinned>=0;popup.Anchor=new Rect(a.x,a.y,b.x-a.x,b.y-a.y);
}
}
else if(popup!=null)popup.Connection=null;
FocusSlot=-1;
if(Parent==null)return;
foreach(var panel in Parent.Children)
if(panel.HasHovered && panel.HasClass("squad-position"))
for(int slot=0;slot<11;slot++)if(panel.HasClass(slotClasses[slot])){FocusSlot=slot;return;}
}
public override void OnDeleted(){popup?.Delete(true);base.OnDeleted();}
protected override int BuildHash()
{
RefreshLinks();
return HashCode.Combine(stamp,Hover,Pinned,FocusSlot,Interacting,GamePreferences.Russian,MathF.Round(Box.Rect.Width*4)/4,MathF.Round(Box.Rect.Height*4)/4)^LayoutStamp();
}
}