PackOpeningView.razor

Razor panel for the pack opening cinematic. Drives reveal timing, skip and spread actions, card layout, and sound cues from PackOpening data.

@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@using System.Globalization
@inherits Panel

<root class="@($"pack-cinema cinema-{(Age>=Show.SignalAt?Show.Material:"neutral")} {(FaceVisible?"face-visible":"face-hidden")} {(Spreading?"cinema-spreading":"cinema-solo")} {(Complete?"cinema-complete":"cinema-running")} {(Age<1.35f?"cinema-intro":"")}")">
    <div class="cinema-backdrop"></div>
    <PackOpeningScene class="cinema-scene" @ref="Stage" Age="@Age" Tier="@Show.Tier" RevealAt="@Show.RevealAt" PackArt="@PackArt" />
    <div class="cinema-vignette"></div>
    <div class="cinema-header"><div><label class="cinema-eyebrow">@Text("ПОПОЛНЕНИЕ КЛУБА","CLUB SIGNING")</label><label class="cinema-pack-name">@PackName</label></div>
        @if(Spreading){<label class="cinema-count">@($"{RevealedCount:00} / {Show.Rewards.Length:00}")</label>}
        else if(!FaceVisible){<button class="cinema-skip-intro" onclick=@RevealBest>@Text("Пропустить анимацию","Skip animation")</button>}
    </div>
    <div class="cinema-card-stage">
    @for(int i=Show.Rewards.Length-1;i>=0;i--)
    {
        int index=i;var reward=Show.Rewards[i];bool star=i==0;bool visible=Show.Visible(i,Age,SpreadAge);
        @if(star?Age>=1.35f:visible)
        {
            <div @key="reward" class="@($"cinema-card-slot {(star?"cinema-star":"cinema-other")} {(visible?"reward-visible":"reward-hidden")}")" style="@CardStyle(index)" onclick=@(()=>CardClick(index))>
                <div class="cinema-card-canvas">
                @if(visible)
                {
                    @if(reward.Player!=null){<PlayerCardView Card="@reward.Player" ClubSeed="@ClubSeed" CssClass="catalog-tile pack-card-face display-only" />}
                    else{<ClubItemCardView Reward="@reward" CssClass="pack-card-face pack-item-face display-only" />}
                }
                else{<div class="cinema-card-back"><div class="cinema-back-border"></div><img src="/ui-icons/ui-bold-nav-ball.svg" /><label>SOCER</label></div>}
                @if(visible && star && !Spreading && Age<Show.RevealAt+1.3f){<div class="cinema-sheen" style="@($"background-position:{F(130-(Age-Show.RevealAt)*150)}% 50%;")"></div>}
                </div>
                @if(visible)
                {<div class="cinema-card-caption"><label class="@(reward.Name.Length>28?"name-long":"")">@(reward.Player!=null?reward.Player.Name:ItemName(reward))</label>@if(reward.Duplicate){<label class="cinema-duplicate">@Text("ПОВТОР","DUPLICATE")</label>}</div>}
            </div>
        }
    }
    </div>
    @if(!FaceVisible && Age>=Show.SignalAt && Show.Star?.Player!=null)
    {<div class="cinema-clues"><img src="@($"/flags/{Show.Star.Player.CountryCode.ToLowerInvariant()}.svg")" />@if(Age>=Show.SignalAt+.55f){<label>@PlayerPositions.Card(Show.Star.Player)</label>}</div>}
    @if(!FaceVisible){<label class="cinema-suspense">@(Age<.95f?Text("Момент перед открытием…","The moment before…"):Age<Show.SignalAt?Text("Кого нашли наши скауты?","Who did our scouts find?"):Age<Show.HoldAt?Text("Под светом прожекторов…","Under the floodlights…"):Text("Встречайте…","Introducing…"))</label>}
    <div class="cinema-impact" style="@($"opacity:{F(Flash)};")"></div>
    <div class="cinema-footer">
        @if(FaceVisible && !Spreading){<button class="cinema-show-pack" onclick=@SpreadPack>@Text("Показать весь набор →","Show the full pack →")</button>}
        else if(Spreading && !Complete){<label class="cinema-unfolding">@Text("Раскрываем набор…","Revealing your pack…")</label>}
        else if(Complete){<button class="reveal-inventory cinema-inventory" onclick=@(()=>OnInventory?.Invoke())>@Text("К составу","Team inventory")</button><button class="reveal-back cinema-continue" onclick=@(()=>OnClose?.Invoke())>@Text("Далее →","Continue →")</button>}
    </div>
</root>

@code {
    [Parameter] public PackOpening Opening {get;set;}
    [Parameter] public string PackArt {get;set;}
    [Parameter] public string PackName {get;set;}
    [Parameter] public int ClubSeed {get;set;}
    [Parameter] public bool Preview {get;set;}
    [Parameter] public float StartedAt {get;set;}
    [Parameter] public Action OnClose {get;set;}
    [Parameter] public Action OnInventory {get;set;}
    PackOpeningPresentation show;
    PackOpeningPresentation Show=>show!=null && ReferenceEquals(show.Receipt,Opening)?show:show=new(Opening);
    PackOpeningScene Stage;
    readonly PackOpeningSound sound=new();
    float offset,lastAge=-1,spreadAt=float.NaN;
    float? frozenAge,frozenSpreadAge;
    PackOpening seen;
    public float Age=>frozenAge??Math.Max(0,Time.Now-StartedAt+offset);
    public float SpreadAge=>frozenSpreadAge??(float.IsNaN(spreadAt)?-1:Math.Max(0,Time.Now-spreadAt));
    public bool Spreading=>SpreadAge>=0;
    public bool FaceVisible=>Age>=Show.RevealAt;
    public bool Complete=>Spreading && SpreadAge>=Show.SpreadDuration;
    public int RevealedCount=>Show.Revealed(Age,SpreadAge);
    static string F(float n)=>n.ToString("0.###",CultureInfo.InvariantCulture);
    static float Ease(float n){n=Math.Clamp(n,0,1);return 1-MathF.Pow(1-n,3);}
    string Text(string ru,string en)=>GamePreferences.Russian?ru:en;
    string ItemName(PackReward r)=>r.Kind switch {"coins"=>Text($"{r.Coins} монет",$"{r.Coins} coins"),"training"=>Text("Карточка улучшения","Upgrade card"),_=>r.Name};
    float Flash=>Math.Clamp(1-Math.Abs(Age-Show.RevealAt-.035f)/.075f,0,1)*.34f;
    float ViewWidth=>Math.Max(320,Box.Rect.Width*ScaleFromScreen);
    float ViewHeight=>Math.Max(320,Box.Rect.Height*ScaleFromScreen);
    float SoloScale=>Math.Min(2.2f,Math.Min(ViewWidth*.58f/174,ViewHeight*.59f/232));
    float RowScale=>Math.Min(1.65f,Math.Min((ViewWidth-96-20*(Show.Rewards.Length-1))/Math.Max(174,174*Show.Rewards.Length),ViewHeight*.5f/232));
    string CardStyle(int index)
    {
        float rowWidth=Show.Rewards.Length*174*RowScale+Math.Max(0,Show.Rewards.Length-1)*20;
        float target=(ViewWidth-rowWidth)*.5f+174*RowScale*.5f+index*(174*RowScale+20);
        float progress=Spreading?Ease(SpreadAge/.65f):0;
        float scale=SoloScale+(RowScale-SoloScale)*progress,x=50,alpha=1,sx=1,tilt=0;
        if(index==0)
        {
            x=50+(target/ViewWidth*100-50)*progress;
            float intro=Math.Clamp((Age-1.35f)/.65f,0,1),flip=Math.Clamp((Age-Show.RevealAt+.22f)/.44f,0,1);
            sx=Math.Max(.025f,Math.Abs(MathF.Cos(flip*MathF.PI)));scale*=.72f+.28f*intro;alpha=intro;
            tilt=FaceVisible?0:MathF.Sin(Age*1.4f)*2;
        }
        else
        {
            float t=Ease((SpreadAge-Show.OtherRevealAt(index))/.62f);
            float behind=50+(((ViewWidth-rowWidth)*.5f+174*RowScale*.5f)/ViewWidth*100-50)*progress;
            x=behind+(target/ViewWidth*100-behind)*t;scale=RowScale*(.88f+.12f*t);alpha=t;tilt=(1-t)*-7;
        }
        return $"left:{F(x)}%;top:46%;z-index:{(index==0?100:50-index)};opacity:{F(alpha)};transform:translate(-50%, -50%) rotate({F(tilt)}deg) scale({F(scale*sx)}, {F(scale)});";
    }
    void CardClick(int index){if(index==0)SpreadPack();}
    public void SpreadPack()
    {
        if(!FaceVisible || Spreading)return;
        frozenSpreadAge=null;spreadAt=Time.Now;GameAudio.Tap();StateHasChanged();
    }
    public void RevealBest()
    {
        float age=Age;frozenAge=null;offset+=Math.Max(0,Show.RevealAt+.3f-age);lastAge=Age;sound.Stop();Stage?.ClearTransientEffects();
    }
    public void RevealNext(){if(FaceVisible)SpreadPack();else RevealBest();}
    public void RevealAll(){RevealBest();frozenSpreadAge=null;spreadAt=Time.Now-Show.SpreadDuration;sound.Stop();Stage?.ClearTransientEffects();}
    public void ReviewAt(float age){frozenAge=Math.Max(0,age);lastAge=Age;sound.Stop();StateHasChanged();}
    public void ReviewSpreadAt(float age){frozenSpreadAge=age;StateHasChanged();}
    public void ReviewPlay(){frozenAge=frozenSpreadAge=null;spreadAt=float.NaN;offset=StartedAt-Time.Now;lastAge=-1;sound.Stop();}
    public void StopPresentation(){sound.Stop();Stage?.ClearTransientEffects();}
    public object Diagnostics()=>new{Age,Show.Tier,Show.Material,Show.RevealAt,Show.CompleteAt,Show.SpreadDuration,SpreadAge,Spreading,Complete,FaceVisible,RevealedCount,SoloScale,RowScale,Star=Show.Star?.Name,StarRating=Show.Star?.Player?.Overall,RewardCount=Show.Rewards.Length,Stage=Stage?.Diagnostics(),Sound=sound.Diagnostics()};
    public PackOpeningView(){StyleSheet.Load("/PackOpeningView.razor.scss");}
    public override void Tick()
    {
        base.Tick();
        if(seen!=Opening){seen=Opening;offset=0;frozenAge=frozenSpreadAge=null;spreadAt=float.NaN;lastAge=-1;sound.Stop();}
        float age=Age;if(!frozenAge.HasValue)sound.Update(age,lastAge,Show.Tier,Show.RevealAt);lastAge=age;StateHasChanged();
    }
    protected override int BuildHash()=>HashCode.Combine(Opening,(int)(Age*60),(int)(SpreadAge*60),GamePreferences.Russian,Box.Rect.Size);
    public override void OnDeleted(){sound.Stop();base.OnDeleted();}
}