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