This Razor component defines the visual structure and behavior for a game card UI element. It renders card properties like title, rarity, art, tags, and rule text, while managing interaction logic such as selection and discarding.
@namespace CitySim.Punchboard
@using Sandbox.UI
@using CitySim.Cards
@using CitySim.DesignSystem
@inherits Panel
<root class="pb-card @DisplayRarity.Css() @Size.Css() @CssClass @(Selected ? "selected" : "") @(Disabled ? "disabled" : "") @(Compact ? "compact" : "") @(DisplayCost.HasValue ? "has-cost" : "")">
<div class="pb-paper"></div>
<div class="pb-rarity-frame"></div>
<div class="pb-foil-line"></div>
@if ( !string.IsNullOrEmpty( HotKey ) )
{
<div class="pb-key">@HotKey</div>
}
@if (DisplayCost.HasValue) { <div class="pb-cost">🪙 @DisplayCost.Value</div> }
@if (Card != null) { <div class="pb-tags">@foreach (var tag in TagChip.Values(Card.Tags)) { <TagChip City=@City Tag=@tag Compact=@true Reference=@true /> }</div> }
else if (TagContent != null) { <div class="pb-tags">@TagContent</div> }
else if ( Tags != null && Tags.Any() ) { <div class="pb-tags">@foreach ( var tag in Tags ) { <PbTagPill Tag=@tag Compact=@true /> }</div> }
<div class="pb-art" style="background-color: @Pb.Rgba( ArtColor )">
<PbIcon Icon=@DisplayIcon />
</div>
<div class="pb-name">@(GameText.Plain(DisplayTitle))</div>
@if (Card != null && !BuildOnly && BuildCost > 0) { <div class="pb-build-cost">@(GameText.Get("ui.plotter_card.to_build_total", BuildCost, (DisplayCost ?? 0) + BuildCost))</div> }
@if ( !string.IsNullOrEmpty( DisplayText ) )
{
<div class="pb-text">
@foreach ( var part in CardRuleText.Split(DisplayText) )
{
<div class="pb-rule-line @(part.Label != null ? "timed" : "")">
@if ( part.Label != null ) { <PbRuleKeyword [email protected] Label=@(GameText.Plain(part.Label)) Help=@(GameText.Plain(part.Help)) /> }
<div class="pb-rule-copy">@if (City != null) { <CardRuleContent City=@City [email protected] />} else {<span>@(GameText.Plain(part.Text))</span> }</div>
</div>
}
</div>
}
@if ( !string.IsNullOrEmpty( DisplayRequirement ) ) { <div class="pb-requirement">@(GameText.Plain(DisplayRequirement))</div> }
<div class="pb-rarity-row">@if (ShowRarity) { <PbRarityMarker Rarity=@DisplayRarity /> } @if (Card != null && !string.IsNullOrEmpty(CardPresentation.Limit(Card))) { <div class="pb-limit">@CardPresentation.Limit(Card)</div> }</div>
@ChildContent
@if (ShowDiscard) { <PbButton CssClass="card-discard" Help=@(GameText.Get("ui.pb_card.discard", DisplayTitle)) Kind=@(PbButtonKind.Ghost) Size=@(PbSize.Small) Disabled=@(Disabled || DiscardDisabled) Clicked=@Discard><ChildContent><div class="discard-icon"><div class="discard-lid"></div><div class="discard-bin"></div></div></ChildContent></PbButton> }
</root>
@code
{
/// <summary>Content placed inside the component.</summary>
[Parameter] public new RenderFragment ChildContent { get; set; }
[Parameter] public CityGame City { get; set; }
[Parameter] public CardDef Card { get; set; }
[Parameter] public Tag GameplayTags { get; set; }
[Parameter] public RenderFragment TagContent { get; set; }
[Parameter] public RenderFragment InteractionContent { get; set; }
[Parameter] public string TypeLabel { get; set; }
[Parameter] public string Interactions { get; set; }
[Parameter] public string Requirement { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public PbSize Size { get; set; } = PbSize.Normal;
[Parameter] public string CssClass { get; set; }
/// <summary>An emoji or glyph, or a texture path relative to Assets.</summary>
[Parameter] public string Icon { get; set; }
[Parameter] public PbRarity Rarity { get; set; }
[Parameter] public bool ShowRarity { get; set; } = true;
[Parameter] public IEnumerable<PbTag> Tags { get; set; }
/// <summary>Rules text printed on the card.</summary>
[Parameter] public string Text { get; set; }
/// <summary>The colour of the art window. Pass the card's main tag or building colour.</summary>
[Parameter] public Color Accent { get; set; } = new Color( 213f / 255, 168f / 255, 61f / 255 );
/// <summary>A small badge such as "1" showing which key selects the card.</summary>
[Parameter] public string HotKey { get; set; }
[Parameter] public int? Cost { get; set; }
[Parameter] public bool BuildOnly { get; set; }
[Parameter] public bool Selected { get; set; }
[Parameter] public bool Disabled { get; set; }
/// <summary>Use the compact card styling.</summary>
[Parameter] public bool Compact { get; set; }
[Parameter] public Action Clicked { get; set; }
[Parameter] public bool ShowDiscard { get; set; }
[Parameter] public bool DiscardDisabled { get; set; }
[Parameter] public Action Discarded { get; set; }
[Parameter] public bool SelectionSound { get; set; } = true;
string DisplayTitle => Card?.Name ?? Title;
string DisplayIcon => Card == null ? Icon : CardPresentation.Artwork(Card);
PbRarity DisplayRarity => Card == null ? Rarity : PunchboardMapping.Rarity(Card.Rarity);
string DisplayText => Card == null ? Text : CardPresentation.Rules(Card);
string DisplayRequirement => Card == null ? Requirement : CardPresentation.Requirement(Card);
int BuildCost => Card == null ? 0 : (City?.Match?.Settings ?? new CitySim.Match.MatchSettings()).BuildCost(Card);
int? DisplayCost => BuildOnly && Card != null ? (BuildCost > 0 ? BuildCost : null) : Cost ?? (Card != null ? City?.Match?.Settings.Rarities.FirstOrDefault(r => r.Rarity == Card.Rarity)?.MinimumBid : null);
Color ArtColor => Color.Lerp( Pb.CardStock, Card == null ? Accent : PunchboardMapping.Accent(Card.Tags), 0.5f );
protected override void OnParametersSet()
{
base.OnParametersSet();
AcceptsFocus = !Disabled;
}
protected override void OnClick( MousePanelEvent e )
{
base.OnClick( e );
if (Descendants.Any(panel => panel.HasHovered && (panel is PbButton or PlotterCardReference or TagChip or InteractionChip))) return;
Activate();
}
public override void OnButtonEvent( ButtonEvent e )
{
base.OnButtonEvent( e );
if ( Disabled || !HasFocus || !e.Pressed || Descendants.Any(panel => panel.HasFocus && (panel is PlotterCardReference or TagChip or InteractionChip)) ) return;
if ( e.Button == "enter" || e.Button == "space" ) Activate();
}
float lastActivation = -100;
void Discard() { if (Disabled || DiscardDisabled || lastActivation == Time.Now) return; lastActivation = Time.Now; Discarded?.Invoke(); }
void Activate() { if (Disabled || Clicked == null || lastActivation == Time.Now) return; lastActivation=Time.Now; if (SelectionSound) TabletopAudio.Card(); Clicked.Invoke(); }
public override void Tick()
{
base.Tick();
SetClass("reference-match", City?.ReferenceTag is { } tag && tag != Tag.None && (Card?.Tags ?? GameplayTags).HasFlag(tag));
}
protected override int BuildHash()
{
var a = HashCode.Combine( DisplayTitle, DisplayIcon, DisplayRarity, DisplayText, HotKey, Selected, Disabled, Compact );
return HashCode.Combine( a, Accent, Tags == null ? 0 : string.Join( ",", Tags ).GetHashCode(), Size, CssClass, ShowRarity, HashCode.Combine(Interactions, DisplayRequirement, DisplayCost, ShowDiscard, DiscardDisabled, BuildCost, BuildOnly) );
}
}