The GoalsPanel UI component displays active match goals and the player's personal contract goals. It uses a summary view that expands to show detailed goal tracking, progress bars, and related contract information.
@namespace CitySim
@using CitySim.DesignSystem
@using CitySim.Match
@using CitySim.Punchboard
@inherits MatchPanel
<root class="goals-panel @(City.Screens.Top == GameScreen.Goals ? "expanded" : "")">
<PlotterButton CssClass="goals-trigger global-action" Size=@(PbSize.Small) Kind=@(PbButtonKind.Tertiary) Clicked=@(() => { City.ToggleGoals(); })><ChildContent><div class="goals-summary"><div class="goals-label">@(GameText.Get("ui.goals_panel.goals"))</div>@foreach (var item in goals) { var def = GoalLibrary.Get(item.GoalId); <div class="goals-summary-item">@(GameText.Plain($"{def.Icon} {def.Name}"))</div> } @if (goals.Count == 0) { <div class="goals-summary-item">@(GameText.Get("ui.goals_panel.no_active_goals"))</div> }</div></ChildContent></PlotterButton>
<div class="pb-goals-layout"><PlotterSurface Title=@(GameText.Get("ui.goals_panel.city_goals"))><ChildContent><div class="pb-goals-content">
<div class="pb-goals-heading"><div>@(GameText.Get("ui.goals_panel.goals"))</div>@if (City.Screens.Top == GameScreen.Goals) { <PlotterButton Text=@(GameText.Get("ui.goals_panel.close")) Size=@(PbSize.Small) Kind=@(PbButtonKind.Ghost) Clicked=@(() => { City.ToggleGoals(); }) /> }</div>
<div class="goals-body pb-goals-scroll">
@foreach (var item in goals) { var def=GoalLibrary.Get(item.GoalId); var progress=GoalRules.Evaluate(BoundMatch,CityGame.Me,def); <div class="pb-goal-item"><PlotterSurface Title=@(GameText.Plain(def.Name))><ChildContent><div class="pb-goal-detail"><div>@(GameText.Plain($"{def.Icon} {def.Description}"))</div>@if (item.Claim != null) { <div>@(GameText.Get("ui.goal_card_panel.round", item.Claim.Round)+string.Join(" · ",item.Claim.Awards.Select(a =>GameText.Get("ui.goal_card_panel.vp", City.Match.Players[a.Player].Name, a.Vp))))</div>} else {<PbMeter Value=@(progress.Target > 0 ? (float)progress.Current/progress.Target : 0f) Label=@(GameText.Get("ui.goals_panel.your_progress")) Text=@($"{progress.Current} / {progress.Target}") /> }</div></ChildContent></PlotterSurface></div> }
<div class="pb-goal-item personal-goal"><PlotterSurface Title=@(GameText.Get("ui.goals_panel.your_unique_goal"))><ChildContent><div class="pb-goal-detail">@if (contract?.Selected is { } id) { var def=Contracts.Get(id); <div>@(GameText.Get("ui.goals_panel.vp", def.Icon, def.Name, def.Vp))</div><div>@(GameText.Plain(def.Description))</div><div>@(GameText.Plain(contract.Progress.Detail))</div>} else {<div>@(City.Mode == GameMode.Sandbox ? GameText.Get("ui.goals_panel.no_contract_in_debug_sandbox") : GameText.Get("ui.goals_panel.pick_a_contract_before_the_first_round"))</div> }</div></ChildContent></PlotterSurface></div>
</div></div></ChildContent></PlotterSurface></div>
</root>
@code {
IReadOnlyList<PublicGoal>goals = Array.Empty<PublicGoal>();
ContractView contract;
bool dirty;
void Dirty() => dirty = true;
void Coin(ResourceChange change) => Dirty();
void Placed(BoardCell cell) => Dirty();
void Changed(Guid player) => Dirty();
void Phase(PhaseChange phase) => Dirty();
void Read() { goals = BoundMatch?.Goals.Active ?? Array.Empty<PublicGoal>(); contract = City?.OwnContract; dirty = false; Refresh(); }
protected override void Subscribe() { if (BoundMatch != null) { BoundMatch.Events.PublicGoalsChanged += Dirty; BoundMatch.Events.ContractChanged += Changed; BoundMatch.Events.CoinsChanged += Coin; BoundMatch.Events.CardPlaced += Placed; BoundMatch.Events.CellUpdated += Placed; BoundMatch.Events.PhaseChanged += Phase; } Read(); }
protected override void Unsubscribe() { if (BoundMatch != null) { BoundMatch.Events.PublicGoalsChanged -= Dirty; BoundMatch.Events.ContractChanged -= Changed; BoundMatch.Events.CoinsChanged -= Coin; BoundMatch.Events.CardPlaced -= Placed; BoundMatch.Events.CellUpdated -= Placed; BoundMatch.Events.PhaseChanged -= Phase; } }
public override void Tick()
{
base.Tick(); if (dirty) Read(); if (City?.Screens.Top != GameScreen.Goals) return;
var direction = (Input.Pressed("CursorDown") ? 1 : 0)-(Input.Pressed("CursorUp") ? 1 : 0);
if (direction != 0 && Descendants.FirstOrDefault(p => p.HasClass("goals-body")) is { } body) body.ScrollTo(body.ScrollOffset+new Vector2(0,direction*100));
}
}