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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user