Add a vanilla-creature filter to card and library search
Filtering for creatures with an empty rules box can't be done from `oracle_cards.oracle_text`: that column joins every face together, so an Adventure card always carries its spell half's text and a naive `oracle_text = ''` test would exclude exactly the cards worth including. Derive it at ingest instead, from the front face's own text — the front is the half that resolves as the creature, and adventure/prepared/transform cards are all cast from it. Exposed as `vanilla_only` behind its own `has_vanilla` capability check rather than folded into `has_universes_beyond`, since it lands in a later reference-schema revision and a db can have one column without the other. Needs a card-data re-download before it does anything.
This commit is contained in:
@@ -477,8 +477,8 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
"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,themes,edhrec_rank,horizontal,
|
||||
image_uri,back_image_uri,default_scryfall_id,power,toughness)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19)",
|
||||
image_uri,back_image_uri,default_scryfall_id,power,toughness,vanilla)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20)",
|
||||
)?;
|
||||
let mut ins_fts =
|
||||
conn.prepare("INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)")?;
|
||||
@@ -610,6 +610,24 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
c.toughness.as_deref().or_else(|| front.and_then(|f| f.toughness.as_deref())),
|
||||
);
|
||||
|
||||
// "Vanilla": a creature whose printed rules box is empty. Judged from
|
||||
// the front face's *own* text, never the joined `oracle_text` above —
|
||||
// an Adventure or Prepared card is a vanilla creature with a spell
|
||||
// stapled on, and the joined column always carries that spell's text.
|
||||
// The front face is the right one to ask: it's the half that resolves
|
||||
// as the creature, and a transforming card is cast from its front.
|
||||
let front_text = match front {
|
||||
Some(f) => f.oracle_text.clone().unwrap_or_default(),
|
||||
None => oracle_text.clone(),
|
||||
};
|
||||
let vanilla = front_type_line
|
||||
.split('—')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.split_whitespace()
|
||||
.any(|w| w == "Creature")
|
||||
&& front_text.trim().is_empty();
|
||||
|
||||
let changed = ins_card.execute(params![
|
||||
oracle_id,
|
||||
c.name,
|
||||
@@ -630,6 +648,7 @@ fn load_oracle_cards(conn: &Connection, reader: impl Read, cancel: &CancelToken)
|
||||
default_scryfall_id,
|
||||
power,
|
||||
toughness,
|
||||
vanilla as i64,
|
||||
])?;
|
||||
// Only emit satellite rows the first time we see this oracle_id.
|
||||
if changed == 1 {
|
||||
@@ -1088,6 +1107,51 @@ mod tests {
|
||||
assert!(oracle_text.contains("draw a card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vanilla_is_judged_from_the_creature_face_not_the_joined_text() {
|
||||
// The `oracle_text` column joins every face, so an Adventure card's
|
||||
// stored text always contains the spell half — the creature half's
|
||||
// emptiness is only visible in `card_faces`. Scute Swarm-style bodies
|
||||
// with a spell attached are exactly the case the filter must catch.
|
||||
let oracle = r#"[
|
||||
{"oracle_id":"oid-bears","name":"Grizzly Bears","cmc":2.0,"colors":["G"],
|
||||
"color_identity":["G"],"type_line":"Creature — Bear","keywords":[],
|
||||
"power":"2","toughness":"2"},
|
||||
{"oracle_id":"oid-adv","name":"Lovestruck Beast","cmc":3.0,"colors":["G"],
|
||||
"color_identity":["G"],"keywords":[],
|
||||
"type_line":"Creature — Beast // Sorcery — Adventure",
|
||||
"card_faces":[
|
||||
{"type_line":"Creature — Beast","colors":["G"],"power":"5","toughness":"5"},
|
||||
{"type_line":"Sorcery — Adventure","colors":["G"],
|
||||
"oracle_text":"Create a 1/1 white Human creature token."}
|
||||
]},
|
||||
{"oracle_id":"oid-flyer","name":"Serra Angel","cmc":5.0,"colors":["W"],
|
||||
"color_identity":["W"],"type_line":"Creature — Angel","keywords":["Flying"],
|
||||
"oracle_text":"Flying, vigilance","power":"4","toughness":"4"},
|
||||
{"oracle_id":"oid-land","name":"Wastes","cmc":0.0,"colors":[],
|
||||
"color_identity":[],"type_line":"Land","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, jsonl(oracle).as_bytes(), &CancelToken::new()).unwrap();
|
||||
|
||||
let vanilla = |id: &str| -> i64 {
|
||||
conn.query_row(
|
||||
"SELECT vanilla FROM oracle_cards WHERE oracle_id = ?1",
|
||||
params![id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(vanilla("oid-bears"), 1, "a plain vanilla creature");
|
||||
assert_eq!(vanilla("oid-adv"), 1, "adventure whose creature half is vanilla");
|
||||
assert_eq!(vanilla("oid-flyer"), 0, "a creature with abilities");
|
||||
assert_eq!(vanilla("oid-land"), 0, "a textless card that isn't a creature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_produced_mana_including_colorless() {
|
||||
let oracle = r#"[
|
||||
|
||||
Reference in New Issue
Block a user