This is a UI component defined in Razor that represents a collectible card in the game. It displays card properties like name, rarity, cost, and rules, and handles mouse and button interactions for activation and discarding.
@namespace CitySim.DesignSystem
@using CitySim.Cards
@using CitySim.Match
@using CitySim.Punchboard
@using Sandbox.UI
@inherits Panel
<root class="plotter-card @CssClass rarity-@(Card?.Rarity.ToString().ToLowerInvariant() ?? "common") @(Compact ? "compact" : "") @(Selected ? "selected" : "") @(Disabled ? "disabled" : "") @(Matches ? "reference-match" : "")">
@if (Card != null) {
<div class="plotter-card-paper"></div>@if (Card.Rarity == Rarity.Legendary) { <div class="plotter-card-foil-window"><div class="plotter-card-foil"></div></div> }<div class="plotter-card-rarity-frame"></div><div class="plotter-card-rarity-inner"></div>
@if (!string.IsNullOrEmpty(HotKey)) { <div class="plotter-card-key">@HotKey</div> }
@if (DisplayCost.HasValue) { <div class="plotter-card-cost">🪙 @DisplayCost.Value</div> }
<div class="plotter-card-tags">@foreach (var tag in TagChip.Values(Card.Tags)) { <PlotterCardReference City=@City Label=@($"{TagChip.Icon(tag)} {GameText.EnumLabel(tag)}") Tag=@tag ReferenceChanged=@ReferenceChanged /> }</div>
<div class="plotter-card-art"><PbIcon Icon=@(CardPresentation.Artwork(Card, Mature)) /></div>
<div class="plotter-card-name">@(GameText.Plain(Card.Name))</div>
@if (!BuildOnly && ActualBuildCost > 0) { <div class="plotter-card-build">@(GameText.Get("ui.plotter_card.to_build_total", ActualBuildCost, (DisplayCost ?? 0) + ActualBuildCost))</div> }
<div class="plotter-card-rules">@foreach (var part in CardRuleText.Split(CardPresentation.Rules(Card))) {
<div class="plotter-card-rule">@if (part.Label != null) { <PbRuleKeyword [email protected] Label=@(GameText.Plain(part.Label)) Help=@(GameText.Plain(part.Help)) /> }
<div class="plotter-card-words"><CardRuleContent City=@City [email protected] ReferenceChanged=@ReferenceChanged TargetsChanged=@TargetsChanged /></div></div>
}</div>
@if (!string.IsNullOrEmpty(Requirement)) { <div class="plotter-card-requirement">@(GameText.Plain(Requirement))</div> }
<div class="plotter-card-footer"><PbRarityMarker Rarity=@(PunchboardMapping.Rarity(Card.Rarity)) />
@if (ShowDiscard) { <PlotterButton Label=@(GameText.Get("ui.plotter_card.discard")) Variant="quiet" Disabled=@(Disabled || DiscardDisabled) Clicked=@Discard /> }
@if (!string.IsNullOrEmpty(CardPresentation.Limit(Card))) { <div class="plotter-card-limit">@CardPresentation.Limit(Card)</div> }
</div>
}
</root>
@code {
[Parameter] public string CssClass { get; set; }
[Parameter] public CardDef Card { get; set; }
[Parameter] public bool Mature { get; set; }
[Parameter] public CityGame City { get; set; }
[Parameter] public int? Cost { get; set; }
[Parameter] public int? BuildCost { get; set; }
[Parameter] public bool BuildOnly { get; set; }
[Parameter] public bool Compact { get; set; }
[Parameter] public bool Selected { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter] public string HotKey { get; set; }
[Parameter] public bool ShowDiscard { get; set; }
[Parameter] public bool DiscardDisabled { get; set; }
[Parameter] public bool SelectionSound { get; set; } = true;
[Parameter] public Action Clicked { get; set; }
[Parameter] public Action Discarded { get; set; }
[Parameter] public Tag ReferenceTag { get; set; }
[Parameter] public Action<Tag> ReferenceChanged { get; set; }
[Parameter] public Action<IReadOnlyList<InteractionTarget>> TargetsChanged { get; set; }
int ActualBuildCost => BuildCost ?? (City?.Match?.Settings ?? new MatchSettings()).BuildCost(Card);
int? DisplayCost => BuildOnly ? (ActualBuildCost > 0 ? ActualBuildCost : null) : Cost ?? (City?.Match?.Settings ?? new MatchSettings()).Rarities.FirstOrDefault(r => r.Rarity == Card.Rarity)?.MinimumBid;
string Requirement => CardPresentation.Requirement(Card);
bool Matches => Card != null && ((ReferenceTag != Tag.None && Card.Tags.HasFlag(ReferenceTag)) || (City?.ReferenceTag is { } tag && tag != Tag.None && Card.Tags.HasFlag(tag)));
protected override void OnParametersSet() { base.OnParametersSet(); AcceptsFocus = !Disabled && Clicked != null; }
protected override void OnClick(MousePanelEvent e) { base.OnClick(e); if (!Descendants.OfType<PlotterButton>().Any(b =>b.HasHovered) && !Descendants.OfType<PlotterCardReference>().Any(b => b.HasHovered)) Activate(); }
public override void OnButtonEvent(ButtonEvent e) { base.OnButtonEvent(e); if (HasFocus && !FocusedChild && e.Pressed && (e.Button == "enter" || e.Button == "space")) Activate(); }
bool FocusedChild => Descendants.Any(p => (p is PlotterCardReference || p is PlotterButton) && p.HasFocus);
float lastActivation = -100;
public void Activate() { if (Disabled || Clicked == null || lastActivation == Time.Now) return; lastActivation = Time.Now; if (SelectionSound) TabletopAudio.Card(); Clicked(); }
void Discard() { if (Disabled || DiscardDisabled || lastActivation == Time.Now) return; lastActivation = Time.Now; Discarded?.Invoke(); }
static int foilSequence;
readonly float foilOffset = (++foilSequence * 0.47f) % 2.4f;
Panel foilPanel;
float foilPhase = -1f;
bool foilEngaged;
float foilBoostAge = -1f;
public override void Tick() {
base.Tick(); SetClass("reference-match", Matches);
if (Card?.Rarity != Rarity.Legendary) return;
if (foilPanel == null || foilPanel.IsDeleting) foilPanel = Descendants.FirstOrDefault(p => p.HasClass("plotter-card-foil"));
if (foilPanel == null) return;
var engaged = HasHovered || HasFocus;
var justEngaged = engaged && !foilEngaged;
foilEngaged = engaged;
if (PlayerPreferences.Current.ReduceFlashing) {
foilBoostAge = -1f;
foilPanel.Style.Set("transform", "translateX(0px)"); foilPanel.Style.Opacity = 0.55f;
return;
}
if (foilPhase < 0f) foilPhase = foilOffset;
if (justEngaged) {
// The gap is invisible; an active pass stays in place and brightens smoothly.
if (foilPhase >= 1.8f) foilPhase = 0f;
foilBoostAge = 0f;
}
foilPhase += Time.Delta;
if (foilPhase >= 2.4f) foilPhase %= 2.4f;
if (foilPhase >= 1.8f) foilBoostAge = -1f;
if (foilBoostAge >= 0f) foilBoostAge += Time.Delta;
var t = Math.Clamp(foilPhase / 1.8f, 0f, 1f);
var eased = t * t * t * (t * (t * 6f - 15f) + 10f);
var envelope = MathF.Sin(t * MathF.PI);
var boost = foilBoostAge < 0f ? 0f : Math.Clamp(foilBoostAge / 0.1f, 0f, 1f);
boost = boost * boost * (3f - 2f * boost);
foilPanel.Style.Set("transform", FormattableString.Invariant($"translateX({-350f + 700f * eased:0.##}px)"));
foilPanel.Style.Opacity = envelope * envelope * (0.72f + 0.28f * boost);
}
protected override int BuildHash() => HashCode.Combine(GameText.LanguageCode, Mature, HashCode.Combine(Card, Cost, BuildCost, BuildOnly, Compact, Selected, Disabled, HashCode.Combine(HotKey, ShowDiscard, DiscardDisabled, ReferenceTag, CssClass)));
}