SquadSkillLinks.razor

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.

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