Prefer Scryfall's representative printing for default card images
Card previews were picking whichever printing happened to load first during ingest (ORDER BY rowid over every printing, promos and foreign versions included), so a card's default image was effectively random. Capture the representative printing's images straight from the Oracle Cards bulk pass instead, and prefer that over the old printings-table guess, falling back gracefully on an older reference.db that predates these columns. Also add docs/printing-tracking-design.md, a design (not yet implemented) for optionally tracking which specific printing/finish a library or deck card is.
This commit is contained in:
150
docs/printing-tracking-design.md
Normal file
150
docs/printing-tracking-design.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Design: optional per-printing tracking
|
||||
|
||||
Status: **not implemented** — design only, for a future session.
|
||||
|
||||
## Context
|
||||
|
||||
`mtgc` currently identifies every owned/decked card purely by `oracle_id`
|
||||
(one row + a quantity column in both `library_cards` and `deck_cards`). There
|
||||
is no way to record *which printing* a physical copy is, or to own multiple
|
||||
copies of the same card across different printings (e.g. one non-foil `2X2`
|
||||
copy and one foil `PRNA` promo copy of the same card).
|
||||
|
||||
Notably, the schema already anticipated this: `library_card_copies`
|
||||
([0001_init.sql](../crates/mtgc-core/src/sql/library/0001_init.sql)) was
|
||||
defined at the very start of the project as "optional per-physical-copy
|
||||
printing detail (the 'hybrid' identity)" — but it has never been read or
|
||||
written by any code path. This document designs activating it.
|
||||
|
||||
This was split out from the "normal default card images" fix (see the ingest
|
||||
change in `crates/mtgc-core/src/ingest.rs` / `db.rs` that made the *default*
|
||||
image for a card come from Scryfall's representative Oracle Cards printing).
|
||||
That fix solves "the default preview looks weird"; this document solves "I
|
||||
want to track/display a *specific* printing for a specific copy or deck."
|
||||
|
||||
**Decision from the requesting conversation:** a tracked copy should record
|
||||
printing **+ finish** (nonfoil/foil/etched). Condition (NM/LP/etc.) is
|
||||
deferred — the dormant table already has a `condition` column, so adding it
|
||||
later is a compatible follow-up, not a redesign.
|
||||
|
||||
## Identity model — "hybrid"
|
||||
|
||||
- `library_cards` **stays authoritative** for totals: `oracle_id` +
|
||||
`quantity_owned`. Every existing oracle-level feature (legality checks, deck
|
||||
analysis, synergy suggestions, over-assignment warnings) keeps working
|
||||
completely untouched.
|
||||
- `library_card_copies` becomes *optional* annotations: each row means "N of
|
||||
my copies of this card are this specific printing + finish." Copies are a
|
||||
subset of `quantity_owned` — a card can have some copies pinned to a
|
||||
printing and the rest left unspecified. The UI must surface that remainder
|
||||
(e.g. "3 owned · 1 tracked (2X2 foil) · 2 unspecified") rather than forcing
|
||||
every copy to be classified.
|
||||
- Decks get a per-card **display preference** — which printing's art to show
|
||||
for that deck slot — independent of quantity/legality. This is deliberately
|
||||
*not* tied to a specific owned copy; it's just "when I look at this deck,
|
||||
show me this version."
|
||||
|
||||
## Schema / migrations
|
||||
|
||||
### library.db (append-only migrations, per the existing convention in `db.rs`)
|
||||
|
||||
- **`0005_copy_quantity_finish.sql`** — add `quantity INTEGER NOT NULL DEFAULT 1`
|
||||
to `library_card_copies`. Without this, the dormant table is one-row-per-copy,
|
||||
which is awkward for e.g. "4 copies, all the same 2X2 nonfoil printing." A
|
||||
quantity column lets one row represent a group of identical copies. (The
|
||||
`finish` column already exists in the original table; `condition` stays,
|
||||
unused for now.)
|
||||
- **`0006_deck_card_printing.sql`** — add `preferred_scryfall_id TEXT` (nullable)
|
||||
to `deck_cards`. Pure display preference, no FK enforcement needed (printings
|
||||
live in the read-only, rebuildable reference.db, not library.db — a dangling
|
||||
reference after a re-ingest should just fall back to the default image, not
|
||||
error).
|
||||
|
||||
### reference.db (edit `reference_schema.sql` directly — rebuilt, not migrated)
|
||||
|
||||
- Add `lang TEXT NOT NULL DEFAULT ''` and `released_at TEXT NOT NULL DEFAULT ''`
|
||||
to `printings`, for picker labels ("English, 2X2, 2022-07-08") and stable
|
||||
chronological ordering. Both fields are already present on Scryfall printing
|
||||
objects, so `load_printings` just needs to read two more fields — no new
|
||||
ingest pass.
|
||||
|
||||
## Core (`mtgc-core`)
|
||||
|
||||
- New `PrintingRef` model: `scryfall_id, set_code, collector_number, lang,
|
||||
finishes, rarity, image_uri, back_image_uri, price_usd, released_at`.
|
||||
- `Catalog::list_printings(oracle_id) -> Vec<PrintingRef>`, ordered by
|
||||
`(released_at DESC, rowid)`, for the picker UI.
|
||||
- Library copy CRUD in `library.rs`:
|
||||
- `set_copy(oracle_id, scryfall_id, finish, quantity)` — upsert a copy group.
|
||||
- `list_copies(oracle_id) -> Vec<LibraryCardCopy>`.
|
||||
- `remove_copy(copy_id)`.
|
||||
- Validation: sum of copy quantities for an `oracle_id` must stay
|
||||
`<= quantity_owned`; reducing `quantity_owned` below the tracked sum should
|
||||
either clamp copies or surface a conflict — pick one and document it before
|
||||
implementing (leaning toward clamping the newest/most-recently-edited copy
|
||||
row, since silently deleting user-entered printing data is worse than
|
||||
losing precise quantity).
|
||||
- Deck: `set_deck_card_printing(deck_id, oracle_id, scryfall_id | null)`.
|
||||
- **Display resolution:** extend the image-fallback chain added for the
|
||||
default-image fix so a deck card with `preferred_scryfall_id` set resolves
|
||||
its image from that specific printing first:
|
||||
`COALESCE(<preferred printing image>, oc.image_uri, <old per-printing
|
||||
subquery>)`. No component changes needed — everything still just reads
|
||||
`card.image_uri` off the `CardRef`/`DeckEntry` it's given.
|
||||
|
||||
## Commands + `src/api.ts`
|
||||
|
||||
- Tauri commands: `list_printings(oracle_id)`, `set_library_copy(...)`,
|
||||
`list_library_copies(oracle_id)`, `remove_library_copy(copy_id)`,
|
||||
`set_deck_card_printing(deck_id, oracle_id, scryfall_id)`.
|
||||
- `src/api.ts`: matching invoke wrappers; new `PrintingRef` and
|
||||
`LibraryCardCopy` TS interfaces; extend `DeckEntry` with an optional
|
||||
`preferred_scryfall_id`.
|
||||
|
||||
## Frontend (`src/`)
|
||||
|
||||
- New `PrintingPicker` modal (in `components.tsx`): given an `oracle_id`, fetch
|
||||
printings and render a thumbnail grid with set / language / finish / price
|
||||
labels; `onPick(scryfallId)`.
|
||||
- **DeckView** ([src/views/DeckView.tsx](../src/views/DeckView.tsx)): a
|
||||
per-card "Choose printing" affordance opens the picker and calls
|
||||
`set_deck_card_printing`; the deck's thumbnail/preview then reflects that
|
||||
printing.
|
||||
- **LibraryBrowser** / **CardEntry**: a per-card "Versions" affordance to
|
||||
assign printing + finish + quantity to copies; show the
|
||||
specified-vs-unspecified breakdown next to the existing quantity stepper.
|
||||
|
||||
## Open questions to settle before implementing
|
||||
|
||||
1. Should the library *list/tile thumbnail* follow a pinned copy's printing
|
||||
(and if a card has multiple different tracked printings, which one wins?),
|
||||
or should the thumbnail always show the oracle default and per-copy art
|
||||
only appear in a detail/expanded view? Recommend the latter — simpler, and
|
||||
avoids an arbitrary tie-break in the common list view.
|
||||
2. Should decklist import/export (`decklist.rs`) round-trip printing
|
||||
preferences (many decklist formats support a `(SET) 123` suffix)? Out of
|
||||
scope for the initial cut; note it as a natural follow-up once the core
|
||||
model exists.
|
||||
3. Quantity-vs-copies conflict resolution (see `set_copy` validation above) —
|
||||
decide the exact clamping behavior before writing the command, and add a
|
||||
unit test that pins it down.
|
||||
|
||||
## Verification plan (once implemented)
|
||||
|
||||
- Rust unit tests: copy CRUD + quantity clamping; preferred-printing image
|
||||
resolution (deck card with `preferred_scryfall_id` set vs. unset vs.
|
||||
dangling after a re-ingest).
|
||||
- Command round-trip smoke test through `src-tauri`.
|
||||
- Manual: assign a printing to a deck card and confirm the deck preview/thumbnail
|
||||
changes to that art; record two copies of one card under two different
|
||||
printings and confirm the library view shows the specified/unspecified
|
||||
breakdown correctly.
|
||||
|
||||
## Critical files
|
||||
|
||||
`crates/mtgc-core/src/sql/library/0005_*.sql`, `0006_*.sql`,
|
||||
`crates/mtgc-core/src/sql/reference_schema.sql`, `crates/mtgc-core/src/ingest.rs`,
|
||||
`crates/mtgc-core/src/library.rs`, `crates/mtgc-core/src/decks.rs`,
|
||||
`crates/mtgc-core/src/model.rs`, `src-tauri/src/commands.rs`, `src/api.ts`,
|
||||
`src/components.tsx`, `src/views/DeckView.tsx`, `src/views/LibraryBrowser.tsx`,
|
||||
`src/views/CardEntry.tsx`.
|
||||
Reference in New Issue
Block a user