Add deck analysis panel with mana production/consumption bars and oracle text search
Threads a new produced_mana/produces_colorless field through the whole card read path (ingest, library, filters, decks, search) so the deck view can show what colors a deck's manabase actually produces, alongside its existing color identity. Also adds a mana consumption bar (colored casting costs of nonland spells) and stacks the mana curve histogram by color. Adds an oracle-text substring filter to the library search, mirroring the existing location filter. Rearranges the deck sidebar list into two rows (name, then format chip + actions) so long names and action buttons no longer collide, and removes the drop shadows from the commander hero panel/image. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -430,6 +430,10 @@ struct ScryOracle {
|
||||
keywords: Vec<String>,
|
||||
#[serde(default)]
|
||||
card_faces: Option<Vec<ScryFace>>,
|
||||
/// What colors of mana this card can add (lands, mana rocks, dorks). A
|
||||
/// top-level-only Scryfall field — not per-face, even on DFCs.
|
||||
#[serde(default)]
|
||||
produced_mana: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -438,6 +442,8 @@ struct ScryFace {
|
||||
type_line: Option<String>,
|
||||
#[serde(default)]
|
||||
colors: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
oracle_text: Option<String>,
|
||||
}
|
||||
|
||||
fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken) -> Result<usize> {
|
||||
@@ -446,8 +452,9 @@ 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)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
(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)",
|
||||
)?;
|
||||
let mut ins_fts =
|
||||
conn.prepare("INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)")?;
|
||||
@@ -502,6 +509,30 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
// Oracle text: top-level, else join faces — double-faced cards (MDFC,
|
||||
// transform, etc.) never carry a top-level oracle_text in Scryfall's
|
||||
// data, only per-face text.
|
||||
let oracle_text = c.oracle_text.clone().unwrap_or_else(|| {
|
||||
c.card_faces
|
||||
.as_ref()
|
||||
.map(|faces| {
|
||||
faces
|
||||
.iter()
|
||||
.filter_map(|f| f.oracle_text.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
// Mana production: colored letters map to the WUBRG bitmask (same as
|
||||
// colors/identity); "C" (colorless) is tracked separately since it
|
||||
// isn't a WUBRG bit. Top-level only — Scryfall doesn't put this on
|
||||
// individual card_faces.
|
||||
let produced = c.produced_mana.as_deref().unwrap_or(&[]);
|
||||
let produced_mana = ColorSet::from_letters(produced);
|
||||
let produces_colorless = produced.iter().any(|s| s.eq_ignore_ascii_case("C"));
|
||||
|
||||
let changed = ins_card.execute(params![
|
||||
oracle_id,
|
||||
c.name,
|
||||
@@ -510,7 +541,9 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
colors.0 as i64,
|
||||
identity.0 as i64,
|
||||
type_line,
|
||||
c.oracle_text.clone().unwrap_or_default(),
|
||||
oracle_text,
|
||||
produced_mana.0 as i64,
|
||||
produces_colorless as i64,
|
||||
])?;
|
||||
// Only emit satellite rows the first time we see this oracle_id.
|
||||
if changed == 1 {
|
||||
@@ -797,6 +830,83 @@ mod tests {
|
||||
assert_eq!(g as u8, crate::model::G);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_faced_cards_fall_back_to_card_faces_for_type_colors_and_text() {
|
||||
// Modal DFCs (and transform cards) never carry a top-level type_line,
|
||||
// colors, or oracle_text in Scryfall's oracle-cards bulk — only
|
||||
// per-face fields. Regression test for a bug where oracle_text had no
|
||||
// fallback (unlike type_line/colors), silently leaving every
|
||||
// double-faced card's oracle_text empty and unsearchable.
|
||||
let oracle = r#"[
|
||||
{"oracle_id":"oid-dfc","name":"Elesh Norn // The Argent Etchings",
|
||||
"cmc":5.0,"color_identity":["W"],"keywords":[],
|
||||
"card_faces":[
|
||||
{"type_line":"Legendary Creature — Phyrexian Praetor","colors":["W"],
|
||||
"oracle_text":"Whenever a source an opponent controls deals damage to you or a permanent you control, exile that source."},
|
||||
{"type_line":"Battle — Siege","colors":[],
|
||||
"oracle_text":"When you defeated The Argent Etchings, draw a card."}
|
||||
]}
|
||||
]"#;
|
||||
|
||||
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 (type_line, colors, oracle_text): (String, i64, String) = conn
|
||||
.query_row(
|
||||
"SELECT type_line, colors, oracle_text FROM oracle_cards WHERE oracle_id='oid-dfc'",
|
||||
[],
|
||||
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(type_line, "Legendary Creature — Phyrexian Praetor // Battle — Siege");
|
||||
assert_eq!(colors as u8, crate::model::W);
|
||||
assert!(oracle_text.contains("an opponent controls deals damage"));
|
||||
assert!(oracle_text.contains("draw a card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_produced_mana_including_colorless() {
|
||||
let oracle = r#"[
|
||||
{"oracle_id":"oid-dual","name":"Overgrown Tomb","cmc":0.0,
|
||||
"color_identity":[],"keywords":[],"produced_mana":["B","G"]},
|
||||
{"oracle_id":"oid-wastes","name":"Wastes","cmc":0.0,
|
||||
"color_identity":[],"keywords":[],"produced_mana":["C"]},
|
||||
{"oracle_id":"oid-vanilla","name":"Grizzly Bears","cmc":2.0,
|
||||
"color_identity":["G"],"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| -> (i64, i64) {
|
||||
conn.query_row(
|
||||
"SELECT produced_mana, produces_colorless FROM oracle_cards WHERE oracle_id=?1",
|
||||
params![id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let (dual_mana, dual_colorless) = fetch("oid-dual");
|
||||
assert_eq!(dual_mana as u8, crate::model::B | crate::model::G);
|
||||
assert_eq!(dual_colorless, 0);
|
||||
|
||||
let (wastes_mana, wastes_colorless) = fetch("oid-wastes");
|
||||
assert_eq!(wastes_mana, 0); // "C" isn't a WUBRG bit
|
||||
assert_eq!(wastes_colorless, 1);
|
||||
|
||||
let (vanilla_mana, vanilla_colorless) = fetch("oid-vanilla");
|
||||
assert_eq!(vanilla_mana, 0);
|
||||
assert_eq!(vanilla_colorless, 0); // no produced_mana field 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