Ui/AuctionLotPanel.razor

The auction lot UI panel displays a single item for auction. It shows card details, current bid status, and buyer tokens. It runs animations for card reveals, buyer presence, and price movement.

@namespace CitySim
@using Sandbox.UI
@using CitySim.Cards
@using CitySim.Match
@using CitySim.Punchboard
@using CitySim.DesignSystem
@inherits Panel

<root class="auction-lot @(Bid.Amount > 0 ? "has-bid" : "")">
	<PlotterCard City=@City Card=@Card CssClass="lot-face" Cost=@(Bid.Lot.MinimumBid) Selected=@Selected Disabled=@(Auction.Ready || Auction.Resolving || !Auction.Eligible || !Bid.Allowed) Clicked=@(() => { Buy(); }) />
	<div class="buyers">@for (var i=0; i<Auction.Players.Count; i++) { var person=Auction.Players[i]; var seat=i; if (person.SelectedLot == LotIndex) { <div class="buyer-token @(person.Player == CityGame.Me ? "own" : "")" style="background-color: @Pb.Rgba(PlayerColours.For(City.Match,person.Player))">@(seat+1)</div> } }</div>
	@if (!Bid.Allowed && Auction.Eligible && !Auction.Ready && !Auction.Resolving && Auction.AvailableCoins >= Bid.Lot.MinimumBid) { <div class="bid-warning">@(GameText.Get("ui.auction_lot_panel.ownership_limit_reached"))</div> }
	else if (Bid.Allowed && !Auction.Ready && !Auction.Resolving && Auction.AvailableCoins < Bid.Lot.MinimumBid + City.Match.Settings.BuildCost(Card)) { <div class="bid-warning">@(GameText.Get("ui.auction_lot_panel.can_buy_now_save_coins_to_build_later"))</div> }
	<div class="legendary-glint">✦</div>
</root>

@code {
	[Parameter] public CityGame City { get; set; }
	[Parameter] public AuctionView Auction { get; set; }
	[Parameter] public OwnLotBid Bid { get; set; }
	[Parameter] public AuctionWin Winner { get; set; }
	[Parameter] public bool Selected { get; set; }
	[Parameter] public bool SkipReveal { get; set; }
	[Parameter] public Action<int> Picked { get; set; }
	public int LotIndex => Bid.Lot.Index;
	CardDef Card => CardLibrary.Get(Bid.Lot.CardId);
	string Marker => Card.Rarity switch { Rarity.Common => "◇", Rarity.Uncommon => "⬟", Rarity.Rare => "◆", _ => "★" };
	int previousId = -1, previousAmount = -1, previousBuyerCount = -1;
	float revealAge, chipAge = 10, buyersAge = 10;
	bool revealed;
	protected override void OnParametersSet()
	{
		base.OnParametersSet();
		if (previousId != Auction.Id) { previousId = Auction.Id; revealAge = SkipReveal ? 1 : -LotIndex*0.15f; revealed = SkipReveal; previousAmount = Bid.Amount; Style.Opacity = SkipReveal ? 1 : 0; previousBuyerCount = -1; }
		if (previousAmount != Bid.Amount) { previousAmount = Bid.Amount; chipAge = 0; }
		var buyerCount = Auction.Players.Count(p => p.SelectedLot == LotIndex);
		if (previousBuyerCount >= 0 && buyerCount > previousBuyerCount) buyersAge = 0;
		previousBuyerCount = buyerCount;
	}
	void Buy() { Picked?.Invoke(LotIndex); if (!Auction.Resolving && !Auction.Ready && Auction.Eligible && Bid.Allowed) City.BuyMarketCard(Auction.Id,LotIndex); }
	public override void Tick()
	{
		base.Tick();
		revealAge += Time.Delta;
		if (revealAge < 0) return;
		if (!revealed) { revealed = true; City.Feedback.RevealCard(Card.Rarity); }
		var duration = Card.Rarity == Rarity.Legendary ? 0.6f : 0.25f;
		var t = Math.Clamp(revealAge/duration,0,1);
		Style.Opacity = 1;
		var ease = 1-MathF.Pow(1-t,3);
		Style.Set("transform",FormattableString.Invariant($"translateY({(1-ease)*26:0.###}px)"));
		buyersAge += Time.Delta;
		if (Descendants.FirstOrDefault(p => p.HasClass("buyers")) is { } buyers)
		{
			var pop = Math.Clamp(buyersAge/0.22f,0,1);
			buyers.Style.Set("transform",FormattableString.Invariant($"scale({1+0.18f*MathF.Sin(pop*MathF.PI):0.###})"));
		}
		chipAge += Time.Delta;
		if (Descendants.FirstOrDefault(p => p.HasClass("purchase-state")) is { } chip)
		{
			var p = Math.Clamp(chipAge/0.12f,0,1)-1;
			var chipEase = 1+2.70158f*p*p*p+1.70158f*p*p;
			chip.Style.Set("transform",FormattableString.Invariant($"scale({1+0.12f*(1-chipEase):0.###})"));
		}
		if (Descendants.FirstOrDefault(p => p.HasClass("legendary-glint")) is { } glint)
			glint.Style.Opacity = !City.Feedback.ReduceFlashing && Card.Rarity == Rarity.Legendary && revealAge < 1 ? MathF.Sin(Math.Clamp(revealAge,0,1)*MathF.PI) : 0;
	}
}