Add deck legality checking, deck/library import-export, and a command palette
Threads Scryfall's per-format legalities through the reference db and adds a deck-page indicator (structural rules + banned/restricted lists) that lists violation reasons inline. Decks and selected library cards can now be exported to/imported from plain-text decklists, with best-effort name resolution and a report of anything that didn't match. Also adds a shuffle- and-draw sample hand modal, and an Alt+/ command palette for jumping to a tab, deck, or card without leaving the keyboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::db::open_reference_for_build;
|
||||
use crate::error::{CoreError, Result};
|
||||
use crate::model::ColorSet;
|
||||
use crate::model::{ColorSet, Legalities};
|
||||
|
||||
const BULK_INDEX_URL: &str = "https://api.scryfall.com/bulk-data";
|
||||
const USER_AGENT: &str = "mtgc/0.1 (personal library catalog)";
|
||||
@@ -434,6 +434,10 @@ struct ScryOracle {
|
||||
/// top-level-only Scryfall field — not per-face, even on DFCs.
|
||||
#[serde(default)]
|
||||
produced_mana: Option<Vec<String>>,
|
||||
/// Format-slug -> status-string (e.g. "legal"/"not_legal"/"banned"/
|
||||
/// "restricted"). See [`crate::model::Legalities`].
|
||||
#[serde(default)]
|
||||
legalities: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -453,8 +457,8 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
let mut ins_card = conn.prepare(
|
||||
"INSERT OR IGNORE INTO oracle_cards
|
||||
(oracle_id,name,name_lower,mana_value,colors,color_identity,type_line,oracle_text,
|
||||
produced_mana,produces_colorless)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
|
||||
produced_mana,produces_colorless,legalities)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)",
|
||||
)?;
|
||||
let mut ins_fts =
|
||||
conn.prepare("INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)")?;
|
||||
@@ -533,6 +537,8 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
let produced_mana = ColorSet::from_letters(produced);
|
||||
let produces_colorless = produced.iter().any(|s| s.eq_ignore_ascii_case("C"));
|
||||
|
||||
let legalities = Legalities::from_scryfall(&c.legalities);
|
||||
|
||||
let changed = ins_card.execute(params![
|
||||
oracle_id,
|
||||
c.name,
|
||||
@@ -544,6 +550,7 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
oracle_text,
|
||||
produced_mana.0 as i64,
|
||||
produces_colorless as i64,
|
||||
legalities.0,
|
||||
])?;
|
||||
// Only emit satellite rows the first time we see this oracle_id.
|
||||
if changed == 1 {
|
||||
@@ -922,6 +929,48 @@ mod tests {
|
||||
assert_eq!(vanilla_colorless, 0); // no produced_mana field at all
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_legalities() {
|
||||
let oracle = r#"[
|
||||
{"oracle_id":"oid-banned","name":"Balance","cmc":1.0,"color_identity":["W"],"keywords":[],
|
||||
"legalities":{"standard":"not_legal","modern":"banned","legacy":"banned","vintage":"restricted","commander":"banned"}},
|
||||
{"oracle_id":"oid-legal","name":"Lightning Bolt","cmc":1.0,"color_identity":["R"],"keywords":[],
|
||||
"legalities":{"modern":"legal","legacy":"legal","vintage":"legal","pauper":"legal"}},
|
||||
{"oracle_id":"oid-nolegal","name":"Some Test Card","cmc":1.0,"color_identity":[],"keywords":[]}
|
||||
]"#;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = dir.path().join("reference.db");
|
||||
let conn = open_reference_for_build(&dest).unwrap();
|
||||
|
||||
load_oracle_cards(&conn, oracle.as_bytes(), &CancelToken::new()).unwrap();
|
||||
|
||||
let fetch = |id: &str| -> Legalities {
|
||||
Legalities(
|
||||
conn.query_row(
|
||||
"SELECT legalities FROM oracle_cards WHERE oracle_id=?1",
|
||||
params![id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
|
||||
let balance = fetch("oid-banned");
|
||||
assert_eq!(balance.status_for("Vintage"), crate::model::LegalStatus::Restricted);
|
||||
assert_eq!(balance.status_for("Modern"), crate::model::LegalStatus::Banned);
|
||||
assert_eq!(balance.status_for("Standard"), crate::model::LegalStatus::NotLegal);
|
||||
|
||||
let bolt = fetch("oid-legal");
|
||||
assert_eq!(bolt.status_for("Modern"), crate::model::LegalStatus::Legal);
|
||||
assert_eq!(bolt.status_for("Standard"), crate::model::LegalStatus::NotLegal); // absent key
|
||||
|
||||
let vanilla = fetch("oid-nolegal");
|
||||
for (_, label) in crate::model::LEGALITY_FORMATS {
|
||||
assert_eq!(vanilla.status_for(label), crate::model::LegalStatus::NotLegal); // no legalities object at all
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_back_face_image_for_double_faced_cards() {
|
||||
// Transform/MDFC cards: two `card_faces`, each with its own image.
|
||||
|
||||
Reference in New Issue
Block a user