SkillCatalogView.razor

A skill catalog panel for the game UI. Filters the unique skill list by search text and player role, switches between normal and gold grades, and shows the selected skill through CardSkillView.

@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Collections.Generic
@using System.Linq
@inherits Panel

<root class="skill-catalog localized-text">
    <div class="skill-catalog-controls">
        <TextEntry class="skill-catalog-search" Value:bind="@SearchText" Placeholder="@T("Поиск скилла","Search skills")" />
        <div class="skill-catalog-filters">
            @foreach(var role in Roles)
            {
                var option=role;
                <button class="skill-catalog-filter [email protected]().ToLowerInvariant() @(RoleFilter==role?"chosen":"")" onclick=@(()=>SetRole(option))>@RoleName(role)</button>
            }
        </div>
        <div class="skill-catalog-grades">
            <button class="skill-catalog-normal @(Grade==1?"chosen":"")" onclick=@(()=>SetGrade(1))>@UniqueSkills.GradeName(1,GamePreferences.Russian)</button>
            <button class="skill-catalog-gold @(Grade==2?"chosen":"")" onclick=@(()=>SetGrade(2))>@UniqueSkills.GradeName(2,GamePreferences.Russian)</button>
        </div>
    </div>
    <div class="skill-catalog-content">
        <div class="skill-catalog-results">
            <div class="skill-catalog-result-heading"><label>@T("Все скиллы","Skill library")</label><label class="skill-catalog-count">@($"{VisibleSkills.Length} / {Definitions.Length}")</label></div>
            @if(VisibleSkills.Length>0)
            {
                <div class="skill-catalog-grid">
                    @foreach(var ability in VisibleSkills)
                    {
                        var skill=ability.Skill;
                        <button class="skill-catalog-tile [email protected]().ToLowerInvariant() skill-grade-@Grade @(Selected==skill?"chosen":"")" onclick=@(()=>Select(skill))>
                            <SkillBadgeView Skill="@skill" Grade="@Grade" CssClass="skill-catalog-icon" />
                            <label class="skill-catalog-name">@ability.Name(GamePreferences.Russian)</label>
                            <label class="skill-catalog-type">@($"{ability.Role(GamePreferences.Russian)} · "+(ability.IsPassive?T("Постоянный","Passive"):T("Приём","Active")))</label>
                        </button>
                    }
                </div>
            }
            else
            {
                <div class="skill-catalog-empty"><label>@T("Скиллы не найдены","No skills found")</label><label>@T("Попробуйте другое название или роль.","Try another name or role.")</label><button class="skill-catalog-reset" onclick=@ClearFilters>@T("Сбросить поиск","Reset search")</button></div>
            }
        </div>
        <div class="skill-catalog-detail">
            @if(Selected!=UniqueSkill.None){<CardSkillView Instance="@DetailInstance" />}
            else{<label class="skill-catalog-detail-empty">@T("Выберите скилл в библиотеке, чтобы увидеть его условия и связки.","Select a skill to see its conditions and combinations.")</label>}
        </div>
    </div>
</root>

@code {
    enum SkillRole { All,Attack,Midfield,Defence,Goalkeeper }
    static readonly SkillRole[] Roles={SkillRole.All,SkillRole.Attack,SkillRole.Midfield,SkillRole.Defence,SkillRole.Goalkeeper};
    static readonly UniqueSkillInfo[] Definitions=UniqueSkills.All.ToArray();
    // These are eligibility pools, not presentation labels. Shared skills are
    // included for every role that can actually receive them from an upgrade.
    static readonly HashSet<UniqueSkill> AttackPool=new(UniqueSkills.Pool(new PlayerCard{PreferredPosition="ST"}));
    static readonly HashSet<UniqueSkill> MidfieldPool=new(UniqueSkills.Pool(new PlayerCard{PreferredPosition="CM"}));
    static readonly HashSet<UniqueSkill> DefencePool=new(UniqueSkills.Pool(new PlayerCard{PreferredPosition="CB"}));
    static readonly HashSet<UniqueSkill> KeeperPool=new(UniqueSkills.Pool(new PlayerCard{PreferredPosition="GK"}));
    UniqueSkillInfo[] VisibleSkills=Definitions;
    SkillRole RoleFilter=SkillRole.All;
    UniqueSkill Selected=Definitions.FirstOrDefault()?.Skill??UniqueSkill.None;
    int Grade=1;
    string search="";
    readonly SkillInstance DetailInstance=new(){Skill=Definitions.FirstOrDefault()?.Skill??UniqueSkill.None,Grade=1};

    public string SearchText
    {
        get=>search;
        set{var next=value??"";if(next==search)return;search=next;ApplyFilters();}
    }
    static string T(string ru,string en)=>GamePreferences.Russian?ru:en;
    static string RoleName(SkillRole role)=>role switch
    {
        SkillRole.Attack=>T("Атака","Attack"),SkillRole.Midfield=>T("Полузащита","Midfield"),SkillRole.Defence=>T("Защита","Defence"),
        SkillRole.Goalkeeper=>T("Вратарь","Goalkeeper"),_=>T("Все","All")
    };
    void SetRole(SkillRole role){if(RoleFilter==role)return;RoleFilter=role;ApplyFilters();}
    void SetGrade(int grade){if(Grade==grade)return;Grade=grade;DetailInstance.Grade=grade;}
    void Select(UniqueSkill skill){Selected=skill;DetailInstance.Skill=skill;}
    void ClearFilters(){search="";RoleFilter=SkillRole.All;ApplyFilters();}

    void ApplyFilters()
    {
        string query=search.Trim().Replace('ё','е').Replace('Ё','Е');
        HashSet<UniqueSkill> pool=RoleFilter switch
        {
            SkillRole.Attack=>AttackPool,SkillRole.Midfield=>MidfieldPool,SkillRole.Defence=>DefencePool,
            SkillRole.Goalkeeper=>KeeperPool,_=>null
        };
        VisibleSkills=pool==null && query.Length==0?Definitions:Definitions.Where(ability=>
            (pool==null || pool.Contains(ability.Skill)) &&
            (query.Length==0 || ability.EnglishName.Contains(query,StringComparison.OrdinalIgnoreCase) ||
            ability.RussianName.Replace('ё','е').Replace('Ё','Е').Contains(query,StringComparison.OrdinalIgnoreCase))).ToArray();
        if(!VisibleSkills.Any(ability=>ability.Skill==Selected))Selected=VisibleSkills.FirstOrDefault()?.Skill??UniqueSkill.None;
        DetailInstance.Skill=Selected;DetailInstance.Grade=Grade;
    }

    // Search and selection invalidate only this small library. Rendering does
    // not follow match frames or allocate a detail view for every skill/grade.
    protected override int BuildHash()=>HashCode.Combine(search,RoleFilter,Grade,Selected,GamePreferences.Russian);
}