Add card USD pricing and a deck maybeboard

Prices come from Scryfall's prices.usd on printings, shown as a hover badge
on card tiles. The price is picked independently of whichever printing
supplies the display image (the cheapest known price across all printings),
since the display printing is often a promo/variant with no listed price.

The maybeboard lets a deck track cards under consideration without counting
them in deck analysis stats or library assignment totals. Cards move between
mainboard and maybeboard via a per-card toggle; maybeboarding the commander
clears commander status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 21:32:25 -05:00
parent e3f8a299de
commit cf9c930b02
14 changed files with 327 additions and 48 deletions

View File

@@ -11,8 +11,8 @@ use crate::Catalog;
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
/// colors, color_identity, type_line, image_uri, back_image_uri, produced_mana,
/// produces_colorless)` into its library parts. Shared by [`Catalog::list_library`]
/// and the criteria search.
/// produces_colorless, price_usd)` into its library parts. Shared by
/// [`Catalog::list_library`] and the criteria search.
pub(crate) fn row_to_library_parts(
r: &rusqlite::Row,
) -> rusqlite::Result<(String, i64, String, CardRef)> {
@@ -32,6 +32,7 @@ pub(crate) fn row_to_library_parts(
back_image_uri: r.get::<_, Option<String>>(9)?.unwrap_or_default(),
produced_mana: ColorSet(r.get::<_, i64>(10)? as u8),
produces_colorless: r.get::<_, i64>(11)? != 0,
price_usd: r.get::<_, Option<f64>>(12)?.unwrap_or_default(),
},
))
}
@@ -74,13 +75,14 @@ impl Catalog {
let mut stmt = self.conn.prepare(&format!(
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
oc.produced_mana, oc.produces_colorless
oc.produced_mana, oc.produces_colorless, {price}
FROM library_cards lc
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
WHERE lc.quantity_owned > 0
ORDER BY oc.name COLLATE NOCASE",
image = crate::db::image_subquery("image_uri"),
back = crate::db::image_subquery("back_image_uri")
back = crate::db::image_subquery("back_image_uri"),
price = crate::db::price_subquery()
))?;
let rows = stmt
.query_map([], row_to_library_parts)?
@@ -199,9 +201,12 @@ impl Catalog {
}
pub(crate) fn assigned_totals_by_oracle(&self) -> Result<HashMap<String, i64>> {
let mut stmt = self
.conn
.prepare("SELECT oracle_id, SUM(quantity) FROM deck_cards GROUP BY oracle_id")?;
// Maybeboard cards aren't actually committed to a deck, so they
// shouldn't count against a card's owned copies.
let mut stmt = self.conn.prepare(
"SELECT oracle_id, SUM(quantity) FROM deck_cards
WHERE is_maybeboard = 0 GROUP BY oracle_id",
)?;
let mut map = HashMap::new();
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?;
for row in rows {
@@ -292,6 +297,54 @@ mod tests {
assert_eq!(elves.card.back_image_uri, "");
}
#[test]
fn price_usd_is_plumbed_through() {
let fx = test_support::make();
let cat = &fx.catalog;
cat.connection()
.execute(
"UPDATE ref.printings SET price_usd = 12.34 WHERE oracle_id = 'sol-ring'",
[],
)
.unwrap();
cat.add_owned("sol-ring", 1).unwrap();
cat.add_owned("llanowar-elves", 1).unwrap();
let lib = cat.list_library().unwrap();
let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
assert_eq!(sol.card.price_usd, 12.34);
// Fixture cards default to no known price.
let elves = lib.iter().find(|c| c.card.oracle_id == "llanowar-elves").unwrap();
assert_eq!(elves.card.price_usd, 0.0);
}
/// Regression test: a card whose *displayed* printing (the one supplying
/// `image_uri`) has no known price shouldn't show as priceless if a
/// different printing does — e.g. a promo/showcase variant with no
/// listed USD price, while the regular printing has one.
#[test]
fn price_uses_cheapest_available_printing_even_if_display_printing_has_none() {
let fx = test_support::make();
let cat = &fx.catalog;
// The fixture's only printing ("print-sol-ring") supplies the image
// and has no price. Add a second, imageless printing with a price.
cat.connection()
.execute(
"INSERT INTO ref.printings (scryfall_id, oracle_id, price_usd)
VALUES ('print-sol-ring-promo', 'sol-ring', 5.67)",
[],
)
.unwrap();
cat.add_owned("sol-ring", 1).unwrap();
let lib = cat.list_library().unwrap();
let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
assert_eq!(sol.card.price_usd, 5.67);
// Image still comes from the original (imaged) printing.
assert_eq!(sol.card.image_uri, "http://img/sol-ring");
}
#[test]
fn location_is_stored_and_listed() {
let fx = test_support::make();