Add deck synergy suggestions, a responsive deck layout, and undoable library bulk-add
Suggestion scoring is fully offline (tribal/mechanic/archetype/mana-base signals derived at ingest, no external synergy database), with an "unusual picks" mode that surfaces underappreciated cards instead of staples. The deck page's top panels now use a CSS grid so wide windows don't waste horizontal space. Library's bulk add-to-deck can clobber existing deck quantities, so it now offers an undo (snackbar + command palette, the latter with no timeout) that restores each card's prior state exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -438,6 +438,10 @@ struct ScryOracle {
|
||||
/// "restricted"). See [`crate::model::Legalities`].
|
||||
#[serde(default)]
|
||||
legalities: std::collections::HashMap<String, String>,
|
||||
/// Scryfall's EDHREC popularity rank (lower = more played); absent for
|
||||
/// some cards. Kept nullable — see `synergy::CardSuggestion::edhrec_rank`.
|
||||
#[serde(default)]
|
||||
edhrec_rank: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -457,8 +461,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,legalities)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)",
|
||||
produced_mana,produces_colorless,legalities,themes,edhrec_rank)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)",
|
||||
)?;
|
||||
let mut ins_fts =
|
||||
conn.prepare("INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)")?;
|
||||
@@ -538,6 +542,7 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
let produces_colorless = produced.iter().any(|s| s.eq_ignore_ascii_case("C"));
|
||||
|
||||
let legalities = Legalities::from_scryfall(&c.legalities);
|
||||
let themes = crate::synergy::compute_themes(&oracle_text);
|
||||
|
||||
let changed = ins_card.execute(params![
|
||||
oracle_id,
|
||||
@@ -551,6 +556,8 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
produced_mana.0 as i64,
|
||||
produces_colorless as i64,
|
||||
legalities.0,
|
||||
themes,
|
||||
c.edhrec_rank,
|
||||
])?;
|
||||
// Only emit satellite rows the first time we see this oracle_id.
|
||||
if changed == 1 {
|
||||
@@ -971,6 +978,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_themes_and_edhrec_rank() {
|
||||
let oracle = r#"[
|
||||
{"oracle_id":"oid-sac","name":"Sac Outlet","cmc":2.0,"color_identity":["B"],"keywords":[],
|
||||
"oracle_text":"Whenever a creature you control dies, each opponent loses 1 life.","edhrec_rank":1200},
|
||||
{"oracle_id":"oid-vanilla","name":"Grizzly Bears","cmc":2.0,"color_identity":["G"],"keywords":[],
|
||||
"oracle_text":"You gain 2 life."}
|
||||
]"#;
|
||||
|
||||
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| -> (i64, Option<i64>) {
|
||||
conn.query_row(
|
||||
"SELECT themes, edhrec_rank FROM oracle_cards WHERE oracle_id=?1",
|
||||
params![id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let (sac_themes, sac_rank) = fetch("oid-sac");
|
||||
assert_ne!(sac_themes & 1, 0, "Sacrifice bit set from payoff phrasing");
|
||||
assert_eq!(sac_rank, Some(1200));
|
||||
|
||||
let (van_themes, van_rank) = fetch("oid-vanilla");
|
||||
// "You gain 2 life." is an action, not the "whenever you gain life"
|
||||
// payoff, so no theme bit; and no edhrec_rank present → NULL.
|
||||
assert_eq!(van_themes, 0);
|
||||
assert_eq!(van_rank, None);
|
||||
}
|
||||
|
||||
#[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