Ui/GoalsPanel.razor

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