The component renders a player card UI. It displays player attributes like name, rarity, position, skills, and statistics based on provided data. It manages drag and click interactions for the card element.
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@inherits Panel
@{ var cardPortrait=PortraitOverride??PlayerAppearance.Portrait(Card); var equippedSkills=Card?.SkillLoadout?.Where(s=>s!=null && UniqueSkills.Get(s.Skill)!=null).Take(UniqueSkills.MaxSlots(Card)).ToArray()??Array.Empty<SkillInstance>(); }
<button @ref="CardButton" class="football-card [email protected]() @(Card.Rarity is "BRONZE" or "SILVER" or "GOLD" or "ELITE" or "LEGENDARY"?"faceted-card":"") @CssClass @(cardPortrait?.EndsWith("/messi-cartoon-test-v1.png")==true?"portrait-messi-cartoon":"") @(Card.Rarity==CardRatings.Legendary?"legendary-"+Card.CatalogId.ToLowerInvariant():"") @(dragging?"drag-source":"")" style="@InlineStyle" onclick=@ClickCard onmousedown=@(e=>CardMouseDown(e)) onmouseup=@(e=>CardMouseUp(e))>
<div class="card-frame-highlight"></div>
<div class="card-grain"></div><div class="card-light"></div><div class="card-engraving"></div>
<div class="card-top"><label class="ovr">@Card.Overall</label><label class="role">@PlayerPositions.Card(Card)</label></div>
@if(Card.IsAcademyLoan){<label class="card-academy-loan localized-text" title="@(GamePreferences.Russian?"Академия · временный игрок":"Academy · temporary player")">@(GamePreferences.Russian?"ВРЕМ.":"TEMP")</label>}
@if(equippedSkills.Length>0){<div @ref="SkillsStrip" class="card-equipped-skills [email protected] localized-text">@foreach(var skill in equippedSkills){<SkillBadgeView Skill="@skill.Skill" Grade="@skill.Grade" CssClass="@($"card-skill-emblem skill-{skill.Skill.ToString().ToLowerInvariant()} {(skill.Skill==HighlightSkill?"skill-just-granted":"")}")" />}</div>}
<div class="card-portrait-window">
@if(cardPortrait is string portrait){<div class="portrait generated-portrait" style="@($"background-image:url('{portrait}');")"></div>}
else{<div class="portrait">@foreach(var part in PlayerPortrait.Parts(ClubSeed,Card)){<div class="portrait-part" style="@part"></div>}</div>}
</div>
<div class="card-data-panel">
<div class="surname @(Card.Name.Split(' ').Last().Length>8?"name-long":"")">@(Card.Name.Split(' ').Last())</div>
<div class="card-bottom">
<div class="card-stats">@foreach(var stat in PlayerCardDetails.Summary(Card)){<div class="card-stat"><label>@stat.Name</label><b>@stat.Value</b></div>}</div>
<div class="card-insignia"><img class="country-flag" src="@($"/flags/{Card.CountryCode.ToLowerInvariant()}.svg")" title="@Card.Country" /></div>
</div>
</div>
</button>
@code {
[Parameter] public PlayerCard Card { get; set; }
[Parameter] public int ClubSeed { get; set; }
[Parameter] public string CssClass { get; set; }="";
[Parameter] public string InlineStyle { get; set; }="";
[Parameter] public string PortraitOverride { get; set; }
// Old saved panels may still pass this parameter through hotload. It no longer renders progression.
[Parameter] public bool ShowLevel { get; set; }
[Parameter] public UniqueSkill HighlightSkill {get;set;}=UniqueSkill.None;
[Parameter] public Action OnSelected { get; set; }
[Parameter] public bool Draggable { get; set; }
[Parameter] public Action OnDragStarted { get; set; }
[Parameter] public Action OnDragMoved { get; set; }
[Parameter] public Action OnDragEnded { get; set; }
Button CardButton {get;set;}
Panel SkillsStrip {get;set;}
bool pressed,dragging,suppressClick;
Vector2 down;
void ClickCard(){SkillHoverOverlay.Forget(CardButton);if(suppressClick){suppressClick=false;return;}OnSelected?.Invoke();}
void CardMouseDown(PanelEvent e)
{
if(e is not MousePanelEvent mouse || mouse.MouseButton!=MouseButtons.Left)return;
SkillHoverOverlay.Interaction(CardButton,true);
if(!Draggable)return;
// The scrollable bench starts its own drag at 5 px and sends an early
// mouseup, before our 8 px card drag can begin. Keep this gesture here.
e.StopPropagation();
pressed=true;dragging=false;suppressClick=false;down=CardButton.MousePosition;
// UI already routes release to the pressed button. Mouse capture would
// lock the OS cursor and return it to the press position after the drop.
if(CardButton.HasMouseCapture)CardButton.SetMouseCapture(false);
}
void CardMouseUp(PanelEvent e)
{
if(e is not MousePanelEvent mouse || mouse.MouseButton!=MouseButtons.Left)return;
SkillHoverOverlay.Interaction(CardButton,false);
if(!pressed)return;
if(dragging){OnDragEnded?.Invoke();suppressClick=true;}
pressed=false;dragging=false;
if(CardButton.HasMouseCapture)CardButton.SetMouseCapture(false);
}
public override void Tick()
{
base.Tick();
if(CardButton!=null && !pressed && !dragging && !(CssClass??"").Contains("display-only"))
SkillHoverOverlay.Track(CardButton,SkillsStrip);
else SkillHoverOverlay.Leave(CardButton);
if(!pressed)return;
if(!Draggable || CardButton==null){if(dragging)OnDragEnded?.Invoke();pressed=false;dragging=false;SkillHoverOverlay.Interaction(CardButton,false);if(CardButton?.HasMouseCapture==true)CardButton.SetMouseCapture(false);return;}
if(!dragging && (CardButton.MousePosition-down).Length>=8){dragging=true;OnDragStarted?.Invoke();}
if(dragging)OnDragMoved?.Invoke();
}
public override void OnDeleted(){SkillHoverOverlay.Forget(CardButton);base.OnDeleted();}
protected override int BuildHash()
{
if(Card==null)return 0;
// Owned cards are mutated in place. The object reference alone cannot
// invalidate this child after filling or replacing a skill slot.
int skills=HashCode.Combine(Card.SkillRerolls,Card.SkillLoadout?.Count??0);
if(Card.SkillLoadout!=null)
foreach(var skill in Card.SkillLoadout)
skills=HashCode.Combine(skills,skill?.Skill,skill?.Grade);
var d=Card.Details;
return HashCode.Combine(
HashCode.Combine(Card.Id,Card.CardKey,Card.Name,Card.CountryCode,Card.Overall,Card.Position,Card.PreferredPosition,skills),
HashCode.Combine(Card.Pace,Card.Shooting,Card.Passing,Card.Dribbling,Card.Defence,Card.Physical,Card.Endurance,Card.Keeping),
HashCode.Combine(Card.Aggression,Card.Composure,Card.PortraitSkin,Card.PortraitHair,Card.PortraitStyle,Card.PortraitBeard,Card.HeightCm),
HashCode.Combine(d?.GkDiving,d?.GkHandling,d?.GkKicking,d?.GkReflexes,d?.GkPositioning),
HashCode.Combine(CssClass,InlineStyle,PortraitOverride,ClubSeed,Draggable,dragging,GamePreferences.Russian))^HashCode.Combine(HighlightSkill,Card.IsAcademyLoan);
}
}