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:
@@ -112,7 +112,7 @@ impl Catalog {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT dc.quantity, dc.is_commander, COALESCE(lc.quantity_owned, 0),
|
||||
oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity,
|
||||
oc.type_line, {image}, {back}
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless
|
||||
FROM deck_cards dc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = dc.oracle_id
|
||||
LEFT JOIN library_cards lc ON lc.oracle_id = dc.oracle_id
|
||||
@@ -136,6 +136,8 @@ impl Catalog {
|
||||
type_line: r.get(8)?,
|
||||
image_uri: r.get::<_, Option<String>>(9)?.unwrap_or_default(),
|
||||
back_image_uri: r.get::<_, Option<String>>(10)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(11)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(12)? != 0,
|
||||
},
|
||||
})
|
||||
})?
|
||||
|
||||
@@ -18,7 +18,8 @@ impl Catalog {
|
||||
pub fn search_library(&self, filter: &LibraryFilter) -> Result<Vec<LibraryCard>> {
|
||||
let mut sql = format!(
|
||||
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back}
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
|
||||
oc.produced_mana, oc.produces_colorless
|
||||
FROM library_cards lc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
|
||||
WHERE lc.quantity_owned > 0",
|
||||
@@ -85,6 +86,10 @@ impl Catalog {
|
||||
sql.push_str(" AND LOWER(lc.location) LIKE ?");
|
||||
vals.push(Value::Text(format!("%{}%", loc.to_lowercase())));
|
||||
}
|
||||
if let Some(txt) = filter.oracle_text.as_ref().map(|t| t.trim()).filter(|t| !t.is_empty()) {
|
||||
sql.push_str(" AND LOWER(oc.oracle_text) LIKE ?");
|
||||
vals.push(Value::Text(format!("%{}%", txt.to_lowercase())));
|
||||
}
|
||||
|
||||
sql.push_str(" ORDER BY oc.name COLLATE NOCASE");
|
||||
|
||||
@@ -265,4 +270,38 @@ mod tests {
|
||||
}).unwrap();
|
||||
assert_eq!(all.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_text_filter_is_case_insensitive_substring() {
|
||||
let fx = owned_fixture();
|
||||
let cat = &fx.catalog;
|
||||
|
||||
// "Add {G}." (Llanowar Elves) and "Add {C}{C}." (Sol Ring) both mention "add".
|
||||
let add_matches = names(&cat.search_library(&LibraryFilter {
|
||||
oracle_text: Some("add".into()),
|
||||
..Default::default()
|
||||
}).unwrap());
|
||||
assert_eq!(add_matches, vec!["Llanowar Elves", "Sol Ring"]);
|
||||
|
||||
// Case-insensitive.
|
||||
let damage = names(&cat.search_library(&LibraryFilter {
|
||||
oracle_text: Some("DAMAGE".into()),
|
||||
..Default::default()
|
||||
}).unwrap());
|
||||
assert_eq!(damage, vec!["Lightning Bolt"]);
|
||||
|
||||
// Deathrite Shaman has empty oracle_text in the fixture, so it never matches.
|
||||
let none = cat.search_library(&LibraryFilter {
|
||||
oracle_text: Some("nonexistent ability".into()),
|
||||
..Default::default()
|
||||
}).unwrap();
|
||||
assert!(none.is_empty());
|
||||
|
||||
// Empty/whitespace text is treated as "no filter".
|
||||
let all = cat.search_library(&LibraryFilter {
|
||||
oracle_text: Some(" ".into()),
|
||||
..Default::default()
|
||||
}).unwrap();
|
||||
assert_eq!(all.len(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,7 +19,9 @@ use std::path::Path;
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub use error::{CoreError, Result};
|
||||
pub use model::{CardRef, ColorSet, Deck, DeckEntry, LibraryCard, LibraryFilter, LibraryStats};
|
||||
pub use model::{
|
||||
CardRef, ColorSet, Deck, DeckEntry, LibraryCard, LibraryFacets, LibraryFilter, LibraryStats,
|
||||
};
|
||||
pub use search::NameIndex;
|
||||
|
||||
/// A handle to an open catalog: the library db with the reference db attached.
|
||||
@@ -66,7 +68,7 @@ pub(crate) mod test_support {
|
||||
}
|
||||
|
||||
/// (oracle_id, name, mana_value, colors, color_identity, type_line,
|
||||
/// types, subtypes, keywords)
|
||||
/// types, subtypes, keywords, oracle_text)
|
||||
pub struct SeedCard {
|
||||
pub oracle_id: &'static str,
|
||||
pub name: &'static str,
|
||||
@@ -77,6 +79,7 @@ pub(crate) mod test_support {
|
||||
pub types: &'static [&'static str],
|
||||
pub subtypes: &'static [&'static str],
|
||||
pub keywords: &'static [&'static str],
|
||||
pub oracle_text: &'static str,
|
||||
}
|
||||
|
||||
pub fn seed_cards() -> Vec<SeedCard> {
|
||||
@@ -91,6 +94,7 @@ pub(crate) mod test_support {
|
||||
types: &["Creature"],
|
||||
subtypes: &["Elf", "Druid"],
|
||||
keywords: &[],
|
||||
oracle_text: "{T}: Add {G}.",
|
||||
},
|
||||
SeedCard {
|
||||
oracle_id: "deathrite-shaman",
|
||||
@@ -102,6 +106,7 @@ pub(crate) mod test_support {
|
||||
types: &["Creature"],
|
||||
subtypes: &["Elf", "Shaman"],
|
||||
keywords: &[],
|
||||
oracle_text: "",
|
||||
},
|
||||
SeedCard {
|
||||
oracle_id: "serra-angel",
|
||||
@@ -113,6 +118,7 @@ pub(crate) mod test_support {
|
||||
types: &["Creature"],
|
||||
subtypes: &["Angel"],
|
||||
keywords: &["Flying", "Vigilance"],
|
||||
oracle_text: "Flying, vigilance",
|
||||
},
|
||||
SeedCard {
|
||||
oracle_id: "lightning-bolt",
|
||||
@@ -124,6 +130,7 @@ pub(crate) mod test_support {
|
||||
types: &["Instant"],
|
||||
subtypes: &[],
|
||||
keywords: &[],
|
||||
oracle_text: "Lightning Bolt deals 3 damage to any target.",
|
||||
},
|
||||
SeedCard {
|
||||
oracle_id: "sol-ring",
|
||||
@@ -135,6 +142,7 @@ pub(crate) mod test_support {
|
||||
types: &["Artifact"],
|
||||
subtypes: &[],
|
||||
keywords: &[],
|
||||
oracle_text: "{T}: Add {C}{C}.",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -153,8 +161,8 @@ pub(crate) mod test_support {
|
||||
rconn
|
||||
.execute(
|
||||
"INSERT INTO oracle_cards
|
||||
(oracle_id,name,name_lower,mana_value,colors,color_identity,type_line)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7)",
|
||||
(oracle_id,name,name_lower,mana_value,colors,color_identity,type_line,oracle_text)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
rusqlite::params![
|
||||
c.oracle_id,
|
||||
c.name,
|
||||
@@ -162,7 +170,8 @@ pub(crate) mod test_support {
|
||||
c.mv,
|
||||
colors,
|
||||
identity,
|
||||
c.type_line
|
||||
c.type_line,
|
||||
c.oracle_text,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -6,12 +6,13 @@ use rusqlite::{params, OptionalExtension};
|
||||
|
||||
use crate::db::REF_DB;
|
||||
use crate::error::Result;
|
||||
use crate::model::{CardRef, ColorSet, LibraryCard, LibraryStats, WUBRG};
|
||||
use crate::model::{CardRef, ColorSet, LibraryCard, LibraryFacets, LibraryStats, WUBRG};
|
||||
use crate::Catalog;
|
||||
|
||||
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
|
||||
/// colors, color_identity, type_line, image_uri, back_image_uri)` into its
|
||||
/// library parts. Shared by [`Catalog::list_library`] and the criteria search.
|
||||
/// colors, color_identity, type_line, image_uri, back_image_uri, produced_mana,
|
||||
/// produces_colorless)` into its library parts. Shared by [`Catalog::list_library`]
|
||||
/// and the criteria search.
|
||||
pub(crate) fn row_to_library_parts(
|
||||
r: &rusqlite::Row,
|
||||
) -> rusqlite::Result<(String, i64, String, CardRef)> {
|
||||
@@ -29,6 +30,8 @@ pub(crate) fn row_to_library_parts(
|
||||
type_line: r.get(7)?,
|
||||
image_uri: r.get::<_, Option<String>>(8)?.unwrap_or_default(),
|
||||
back_image_uri: r.get::<_, Option<String>>(9)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(10)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(11)? != 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -70,7 +73,8 @@ impl Catalog {
|
||||
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back}
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
|
||||
oc.produced_mana, oc.produces_colorless
|
||||
FROM library_cards lc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
|
||||
WHERE lc.quantity_owned > 0
|
||||
@@ -144,6 +148,29 @@ impl Catalog {
|
||||
})
|
||||
}
|
||||
|
||||
/// Distinct type/subtype/keyword values among owned cards, for filter
|
||||
/// autocomplete.
|
||||
pub fn library_facets(&self) -> Result<LibraryFacets> {
|
||||
Ok(LibraryFacets {
|
||||
types: self.distinct_owned_facet("card_types", "type")?,
|
||||
subtypes: self.distinct_owned_facet("card_subtypes", "subtype")?,
|
||||
keywords: self.distinct_owned_facet("card_keywords", "keyword")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// `table`/`column` are always one of the three hardcoded call sites above
|
||||
/// (never user input), so interpolating them into the query is safe.
|
||||
fn distinct_owned_facet(&self, table: &str, column: &str) -> Result<Vec<String>> {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT DISTINCT x.{column}
|
||||
FROM {REF_DB}.{table} x
|
||||
WHERE x.oracle_id IN (SELECT oracle_id FROM library_cards WHERE quantity_owned > 0)
|
||||
ORDER BY x.{column} COLLATE NOCASE"
|
||||
))?;
|
||||
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
|
||||
Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
/// Set the free-text storage location for a card (creates the library row if
|
||||
/// absent). Empty string clears it.
|
||||
pub fn set_location(&self, oracle_id: &str, location: &str) -> Result<()> {
|
||||
@@ -207,6 +234,22 @@ mod tests {
|
||||
assert_eq!(sol.card.image_uri, "http://img/sol-ring");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_facets_only_include_owned_cards() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
// Own llanowar-elves (Creature — Elf Druid) and lightning-bolt
|
||||
// (Instant); leave serra-angel (Creature — Angel, Flying/Vigilance)
|
||||
// unowned.
|
||||
cat.add_owned("llanowar-elves", 1).unwrap();
|
||||
cat.add_owned("lightning-bolt", 1).unwrap();
|
||||
|
||||
let facets = cat.library_facets().unwrap();
|
||||
assert_eq!(facets.types, vec!["Creature", "Instant"]);
|
||||
assert_eq!(facets.subtypes, vec!["Druid", "Elf"]);
|
||||
assert!(facets.keywords.is_empty()); // Flying/Vigilance only on the unowned Serra Angel
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_stats_aggregates_counts_and_colors() {
|
||||
let fx = test_support::make();
|
||||
|
||||
@@ -91,6 +91,14 @@ pub struct CardRef {
|
||||
/// URL of the *back face* image for the same printing (transform / modal
|
||||
/// double-faced / reversible cards), or empty for single-faced cards.
|
||||
pub back_image_uri: String,
|
||||
/// WUBRG bitmask of colors this card can *produce* (lands, mana rocks,
|
||||
/// dorks, etc.) — independent of `colors`/`color_identity`, which
|
||||
/// describe what color the card *is*, not what it taps for.
|
||||
pub produced_mana: ColorSet,
|
||||
/// Whether this card can produce colorless mana (Scryfall's
|
||||
/// `produced_mana` including "C"), tracked separately since colorless
|
||||
/// isn't a WUBRG bit.
|
||||
pub produces_colorless: bool,
|
||||
}
|
||||
|
||||
/// A library row: a card the user owns, with quantity and its deck memberships.
|
||||
@@ -155,6 +163,16 @@ pub struct LibraryStats {
|
||||
pub colorless_count: i64,
|
||||
}
|
||||
|
||||
/// Distinct type/subtype/keyword values found among *owned* cards, sorted.
|
||||
/// Powers autocomplete for the library filter panel — e.g. no point
|
||||
/// suggesting "Phyrexian" as a subtype if you don't own any.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct LibraryFacets {
|
||||
pub types: Vec<String>,
|
||||
pub subtypes: Vec<String>,
|
||||
pub keywords: Vec<String>,
|
||||
}
|
||||
|
||||
/// Criteria for searching the owned library. All fields are optional; an empty
|
||||
/// filter matches every owned card.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
@@ -184,4 +202,6 @@ pub struct LibraryFilter {
|
||||
/// Case-insensitive substring match against the card's stored location
|
||||
/// (e.g. "Box 1"). Cards with no location set never match a non-empty value.
|
||||
pub location: Option<String>,
|
||||
/// Case-insensitive substring match against the card's oracle rules text.
|
||||
pub oracle_text: Option<String>,
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl NameIndex {
|
||||
pub fn build(conn: &Connection) -> Result<Self> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity,
|
||||
oc.type_line, {image}, {back}
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless
|
||||
FROM {REF_DB}.oracle_cards oc",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri")
|
||||
@@ -51,6 +51,8 @@ impl NameIndex {
|
||||
type_line: r.get(5)?,
|
||||
image_uri: r.get::<_, Option<String>>(6)?.unwrap_or_default(),
|
||||
back_image_uri: r.get::<_, Option<String>>(7)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(8)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(9)? != 0,
|
||||
},
|
||||
})
|
||||
})?
|
||||
|
||||
@@ -10,7 +10,9 @@ CREATE TABLE IF NOT EXISTS oracle_cards (
|
||||
colors INTEGER NOT NULL DEFAULT 0, -- WUBRG bitmask
|
||||
color_identity INTEGER NOT NULL DEFAULT 0, -- WUBRG bitmask
|
||||
type_line TEXT NOT NULL DEFAULT '',
|
||||
oracle_text TEXT NOT NULL DEFAULT ''
|
||||
oracle_text TEXT NOT NULL DEFAULT '',
|
||||
produced_mana INTEGER NOT NULL DEFAULT 0, -- WUBRG bitmask (lands/rocks/dorks)
|
||||
produces_colorless INTEGER NOT NULL DEFAULT 0 -- boolean: can produce {C}
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_oracle_name_lower ON oracle_cards(name_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_oracle_ci ON oracle_cards(color_identity);
|
||||
|
||||
Reference in New Issue
Block a user