151 lines
7.7 KiB
Markdown
151 lines
7.7 KiB
Markdown
|
|
# 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`.
|