Ui/ContractPickPanel.razor

Blazor component that controls the contract selection UI during a match. It displays contract options, handles user selection via clicks and input, and manages the confirmation state.

@namespace CitySim
@using CitySim.DesignSystem
@using Sandbox.UI
@using CitySim.Match
@using CitySim.Punchboard
@inherits MatchPanel
<root class="contract-stage overlay">
 @if (view != null) {
 <div class="pb-contract-layout" style=@($"max-width: {PlayerPreferences.SurfacePixels(1200)};")><PlotterSurface Title=@(GameText.Get("ui.contract_pick_panel.let_s_begin")) Kind=@(PbPanelKind.Raised)><ChildContent><div class="pb-contract-content">
  <div class="opening-summary"><div>@(GameText.Get("ui.contract_pick_panel.starting_coins_rounds", City.Match.Settings.StartingCoins, City.Match.Settings.Rounds))</div></div>
  <div class="pb-contract-title">@(Kept ? GameText.Get("ui.contract_pick_panel.your_plan_is_ready") : GameText.Get("ui.contract_pick_panel.choose_your_secret_plan"))</div>
  <div>@(Kept ? GameText.Get("ui.contract_pick_panel.waiting_for_the_other_players_only_you_can_see_your_plan") : GameText.Get("ui.contract_pick_panel.choose_one_complete_its_goal_to_earn_bonus_points_at_the_end_of_the_game"))</div>
  <div class="pb-contract-scroll"><div class="pb-contract-choices">
  @for (var i=0;i<view.Choices.Count;i++) {
   var index=i; var card=Contracts.Get(view.Choices[i]);
   @if (!Kept || view.Selected==card.Id) {
   <div class="pb-contract-card"><PlotterButton CssClass="contract-choice" Kind=@(PbButtonKind.Secondary) Toggled=@(index==selected) Disabled=@(Kept || confirming) Clicked=@(() => Select(index))><ChildContent><div class="contract-face"><div class="contract-type">@(GameText.Get("ui.contract_pick_panel.secret_plan"))</div><div class="contract-art">@card.Icon</div><div class="contract-name">@(GameText.Plain(card.Name))</div><div class="contract-text">@(GameText.Plain(card.Description))</div><div class="contract-reward">@(GameText.Get("ui.contract_pick_panel.vp_bonus", card.Vp))</div></div></ChildContent></PlotterButton></div>
   }
  }
  </div>
  @if (confirming || Kept) { <div class="contract-stamp">@(GameText.Get("ui.contract_pick_panel.kept_secret"))</div> }
  <div class="pb-contract-subtitle">@(GameText.Get("ui.contract_pick_panel.goals_for_everyone"))</div>
  <div class="goal-reward-note"><div>@(GameText.Get("ui.contract_pick_panel.complete_a_goal_first_to_earn_vp", City.Match.Settings.GoalVp))</div><div>@(GameText.Get("ui.contract_pick_panel.finish_in_the_same_round_as_another_player_you_each_earn_vp", City.Match.Settings.TiedGoalVp))</div></div>
  <div class="pb-contract-goals">@foreach (var goal in City.Match.Goals.Active) { var def=GoalLibrary.Get(goal.GoalId); <div class="pb-contract-goal"><PlotterSurface Title=@(GameText.Plain(def.Name))><ChildContent><div>@(GameText.Plain($"{def.Icon} {def.Description}"))</div></ChildContent></PlotterSurface></div> }</div>
  </div>
  <div class="pb-contract-actions"><PlotterButton Text=@(City.Multiplayer ? GameText.Get("ui.contract_pick_panel.leave_match") : GameText.Get("ui.contract_pick_panel.back_to_menu")) Kind=@(PbButtonKind.Ghost) Clicked=@(() => City.RequestMenu()) />
  @if (!Kept && view.Choices.Count>0) { <PlotterButton CssClass="primary keep-contract" Text=@(GameText.Get("ui.contract_pick_panel.choose", Contracts.Get(view.Choices[selected]).Name)) Disabled=@confirming Clicked=@Confirm /> }
  </div>
 </div></ChildContent></PlotterSurface></div>
 }
</root>

@code {
 ContractView view;
 int selected;
 bool confirming;
 float age, sealAge;
 string pendingContract;
 int pendingDraft;
 bool Kept => view?.Selected!=null;
 void Read() { view=City?.OwnContract; selected=Math.Clamp(selected,0,Math.Max(0,(view?.Choices.Count ?? 0)-1)); if (Kept) selected=Math.Max(0,view.Choices.ToList().IndexOf(view.Selected)); Refresh(); }
 void Select(int index) { if (Kept || confirming || selected==index) return; selected=index; TabletopAudio.Card(); Refresh(); }
 void Confirm() {
  if (view==null || Kept || confirming || selected>=view.Choices.Count) return;
  pendingContract=view.Choices[selected]; pendingDraft=view.DraftId; confirming=true; sealAge=0;
  City.Feedback.RevealContract(); Input.Clear("ConfirmCard"); Input.Clear("ReadyRound"); Refresh();
 }
 protected override void Subscribe() { age=0; confirming=false; if (BoundMatch!=null) BoundMatch.Events.ContractChanged+=Changed; Read(); if (view != null && !Kept) TabletopAudio.Fan(); }
 protected override void Unsubscribe() { if (BoundMatch!=null) BoundMatch.Events.ContractChanged-=Changed; }
 void Changed(Guid owner) { if (owner==CityGame.Me) Read(); }
 public override void Tick() {
  base.Tick(); if (City?.Screens.Top!=GameScreen.ContractPick || view==null) return;
  age+=Time.Delta;
  var layout=Descendants.FirstOrDefault(p=>p.HasClass("pb-contract-layout"));
  if (layout!=null && !City.Feedback.ReduceFlashing) { var t=Math.Clamp(age/0.35f,0,1); layout.Style.Set("transform",FormattableString.Invariant($"translateY({16*MathF.Pow(1-t,3):0.##}px)")); }
  if (confirming) {
   sealAge+=Time.Delta;
   if (!City.Feedback.ReduceFlashing && Descendants.FirstOrDefault(p=>p.HasClass("pb-contract-choices")) is { } cards) {
    var t=Math.Clamp(sealAge/0.5f,0,1); var ease=t*t*(3-2*t);
    cards.Style.Set("transform",FormattableString.Invariant($"translateY({8*ease:0.##}px) scale({1-0.04f*ease:0.###})"));
   }
   if (sealAge>=0.5f) { confirming=false; City.KeepContract(pendingDraft,pendingContract); Refresh(); }
   return;
  }
  if (Descendants.FirstOrDefault(p=>p.HasClass("pb-contract-choices")) is { } restingCards) restingCards.Style.Set("transform","scale(1)");
  if (Kept || view.Choices.Count==0) return;
  var choices=Descendants.OfType<PlotterButton>().Where(p => p.HasClass("contract-choice")).ToArray();
  for(var i=0;i<choices.Length;i++) if (choices[i].HasFocus && selected!=i) Select(i);
  var direction=(Input.Pressed("CursorRight") ? 1 : 0)-(Input.Pressed("CursorLeft") ? 1 : 0);
  if(direction!=0) { City.Scene.GetAllComponents<Hud>().FirstOrDefault()?.FocusBoard(); Select((selected+direction+view.Choices.Count)%view.Choices.Count); }
  if(Input.Pressed("ConfirmCard") || Input.Pressed("ReadyRound")) Confirm();
 }
}