The BoardOverlay is a UI panel that visualizes match claims and cell highlights. It displays detailed information when the player hovers over a specific board cell.
using CitySim.Match;
using CitySim.Cards;
using Sandbox.UI;
using CitySim.DesignSystem;
namespace CitySim;
public sealed class BoardOverlay : MatchPanel
{
readonly List<(TileClaim Claim,SceneLabel Label)> claims = new();
readonly Dictionary<(int X,int Y),SceneLabel> highlightLabels = new();
Panel inspection;
Label inspectionOwner;
PlotterCard inspectionCard;
BoardCell inspectedCell;
float hoverStarted;
protected override void Subscribe() { if (BoundMatch != null) BoundMatch.Events.ClaimsChanged += Read; if (City?.Player != null) City.Player.PreviewChanged += Inspect; if (City != null) City.UiChanged += Inspect; Read(); Inspect(); }
protected override void Unsubscribe() { if (BoundMatch != null) BoundMatch.Events.ClaimsChanged -= Read; if (City?.Player != null) City.Player.PreviewChanged -= Inspect; if (City != null) City.UiChanged -= Inspect; foreach (var item in claims) item.Label.Destroy(); claims.Clear(); foreach (var label in highlightLabels.Values) label.Destroy(); highlightLabels.Clear(); }
void Inspect()
{
if (inspection == null)
{
inspection=AddChild<Panel>("board-inspect");
inspectionOwner=inspection.AddChild<Label>("inspect-owner");
inspectionCard=inspection.AddChild<PlotterCard>();
inspectionCard.BuildOnly=true;
}
var hover=City?.Player?.Hovered;
var cell=hover == null ? null : BoundMatch?.Get(hover.X,hover.Y);
var allowed=City?.Screens.Top==GameScreen.Match && City.State==GameState.Playing &&
(BoundMatch?.Phase != MatchPhase.Bid || Hud.MarketPeeking);
if (!allowed) cell=null;
if (cell != inspectedCell)
{
inspectedCell=cell;
hoverStarted=Time.Now;
inspection.SetClass("visible",false);
}
var visible=cell != null && Time.Now-hoverStarted >= 0.4f;
inspection.SetClass("visible",visible);
if (!visible) return;
inspectionCard.City=City;
inspectionCard.Card=CardLibrary.Get(cell.CardId);
inspectionCard.Mature=Growth.IsMature(cell);
inspectionOwner.Text=BoundMatch.Players.TryGetValue(cell.Owner,out var player) ? player.Name : GameText.Get("ui.board_overlay.city_road");
}
void Read()
{
foreach (var item in claims) item.Label.Destroy(); claims.Clear();
if (BoundMatch == null) return;
foreach (var claim in BoundMatch.Claims)
{
var label = new SceneLabel(City, "Claim", 0.10f); claims.Add((claim,label));
}
}
public override void Tick()
{
base.Tick();
Inspect();
var showHighlights = City?.Player != null && City.State == GameState.Playing && City.Screens.Top == GameScreen.Match &&
(City.Match?.Phase != MatchPhase.Bid || Hud.MarketPeeking);
var cells = showHighlights ? City.HighlightedCells : Array.Empty<BoardCell>();
var wanted = cells.Select(c => (c.X,c.Y)).ToHashSet();
foreach (var key in highlightLabels.Keys.Where(key => !wanted.Contains(key)).ToArray()) { highlightLabels[key].Destroy(); highlightLabels.Remove(key); }
foreach (var cell in cells)
{
var key=(cell.X,cell.Y);
if (!highlightLabels.TryGetValue(key,out var label)) { label=new SceneLabel(City, "Highlight", 0.14f); highlightLabels[key]=label; }
label.Set(SceneLabel.AboveTile(City.Grid.Get(cell.X,cell.Y), City.Grid.CellToWorld(cell.X,cell.Y))+Vector3.Up*18, City.HighlightIcon);
}
foreach (var item in claims)
{
var visible = City.State == GameState.Playing && City.Screens.Top == GameScreen.Match && (City.Match?.Phase != MatchPhase.Bid || Hud.MarketPeeking);
item.Label.Set(visible ? SceneLabel.AboveTile(City.Grid.Get(item.Claim.X,item.Claim.Y), City.Grid.CellToWorld(item.Claim.X,item.Claim.Y))+Vector3.Up*18 : null, $"🚩 {BoundMatch.Players[item.Claim.Owner].Name}");
}
}
}