Deck builder: cost tiles for unowned cards and cards in other decks

Idea and Building decks get two more Deck analysis tiles: what the copies
you don't own would cost, and what the owned copies other building/built
decks are holding are worth. Both count copies, not cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:03:45 -05:00
parent 04dc4f6e7b
commit 8a69ca9f73
2 changed files with 20 additions and 3 deletions

View File

@@ -6,6 +6,10 @@ import { COLORS, Deck, DeckEntry, isPhysical, legalStatusFor, Partner } from "./
/** Copies of a deck card you'd need to acquire to cover the deck. */ /** Copies of a deck card you'd need to acquire to cover the deck. */
export const shortBy = (e: DeckEntry) => Math.max(0, e.quantity - e.quantity_owned); export const shortBy = (e: DeckEntry) => Math.max(0, e.quantity - e.quantity_owned);
/** Owned copies of a deck card you can't use because other building/built
* decks claim them. */
export const claimedElsewhere = (e: DeckEntry) => Math.max(0, Math.min(e.quantity, e.quantity_owned) - e.quantity_spare);
export interface DeckStats { export interface DeckStats {
totalCards: number; totalCards: number;
uniqueCards: number; uniqueCards: number;
@@ -14,6 +18,10 @@ export interface DeckStats {
/** USD for every copy in the deck, commander included; cards without a /** USD for every copy in the deck, commander included; cards without a
* known price count as $0. */ * known price count as $0. */
totalCost: number; totalCost: number;
/** USD for the copies you'd need to buy (see `shortBy`). */
unownedCost: number;
/** USD for the owned copies other decks are holding (see `claimedElsewhere`). */
elsewhereCost: number;
curve: { curve: {
label: string; label: string;
count: number; count: number;
@@ -102,9 +110,10 @@ export function computeDeckStats(contents: DeckEntry[]): DeckStats {
} }
// Unlike the rest, cost counts the commander: it's a card you have to buy. // Unlike the rest, cost counts the commander: it's a card you have to buy.
const totalCost = contents const mainboard = contents.filter((e) => !e.is_maybeboard);
.filter((e) => !e.is_maybeboard) const totalCost = mainboard.reduce((sum, e) => sum + e.price_usd * e.quantity, 0);
.reduce((sum, e) => sum + e.price_usd * e.quantity, 0); const unownedCost = mainboard.reduce((sum, e) => sum + e.price_usd * shortBy(e), 0);
const elsewhereCost = mainboard.reduce((sum, e) => sum + e.price_usd * claimedElsewhere(e), 0);
return { return {
totalCards, totalCards,
@@ -112,6 +121,8 @@ export function computeDeckStats(contents: DeckEntry[]): DeckStats {
landCount, landCount,
avgManaValue, avgManaValue,
totalCost, totalCost,
unownedCost,
elsewhereCost,
curve, curve,
colorCounts, colorCounts,
colorlessCount, colorlessCount,

View File

@@ -1299,6 +1299,12 @@ function DeckBuilder({
<StatTile value={deckStats.landCount} label="lands" /> <StatTile value={deckStats.landCount} label="lands" />
<StatTile value={deckStats.avgManaValue.toFixed(2)} label="avg MV" /> <StatTile value={deckStats.avgManaValue.toFixed(2)} label="avg MV" />
<StatTile value={formatCost(deckStats.totalCost)} label="cost" /> <StatTile value={formatCost(deckStats.totalCost)} label="cost" />
{(deck.status === "idea" || deck.status === "building") && (
<>
<StatTile value={formatCost(deckStats.unownedCost)} label="not owned" />
<StatTile value={formatCost(deckStats.elsewhereCost)} label="in other decks" />
</>
)}
</div> </div>
<div className="deck-stats-body"> <div className="deck-stats-body">
<div className="deck-stats-numbers"> <div className="deck-stats-numbers">