//! Shared domain types used across the core. use serde::{Deserialize, Serialize}; /// A card's color identity / colors, encoded as a 5-bit WUBRG mask. /// /// Bit layout (little end first): W=1, U=2, B=4, R=8, G=16. /// Storing colors this way turns commander color-identity compatibility into a /// single integer predicate (see [`ColorSet::is_subset_of`]). pub const W: u8 = 1 << 0; pub const U: u8 = 1 << 1; pub const B: u8 = 1 << 2; pub const R: u8 = 1 << 3; pub const G: u8 = 1 << 4; /// The five colors in canonical WUBRG order, paired with their single-letter code. pub const WUBRG: [(u8, char); 5] = [(W, 'W'), (U, 'U'), (B, 'B'), (R, 'R'), (G, 'G')]; /// A set of colors as a WUBRG bitmask. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct ColorSet(pub u8); impl ColorSet { pub const COLORLESS: ColorSet = ColorSet(0); /// Build from an iterator of single-letter color codes (e.g. Scryfall's /// `["B", "G"]`). Unknown letters are ignored. pub fn from_letters(letters: I) -> Self where I: IntoIterator, S: AsRef, { let mut mask = 0u8; for l in letters { match l.as_ref() { "W" | "w" => mask |= W, "U" | "u" => mask |= U, "B" | "b" => mask |= B, "R" | "r" => mask |= R, "G" | "g" => mask |= G, _ => {} } } ColorSet(mask) } /// Parse from a compact string like "BG" or "wubrg". Order-insensitive. pub fn parse(s: &str) -> Self { Self::from_letters(s.chars().map(|c| c.to_string())) } /// Canonical WUBRG string, e.g. `ColorSet(B|G)` -> "BG". Colorless -> "". pub fn to_letters(self) -> String { WUBRG .iter() .filter(|(bit, _)| self.0 & bit != 0) .map(|(_, ch)| *ch) .collect() } pub fn is_colorless(self) -> bool { self.0 == 0 } pub fn contains(self, bit: u8) -> bool { self.0 & bit != 0 } /// True if every color in `self` is also in `other`. /// /// This is the commander-legality rule: a card is playable in a commander of /// color identity `C` iff `card_identity.is_subset_of(C)`. pub fn is_subset_of(self, other: ColorSet) -> bool { self.0 & !other.0 == 0 } } /// A card as returned by search / library queries. Oracle-level (one per /// `oracle_id`), enriched with the caller's ownership + deck context where relevant. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CardRef { pub oracle_id: String, pub name: String, pub mana_value: f64, pub colors: ColorSet, pub color_identity: ColorSet, pub type_line: String, /// URL of a card image (Scryfall "normal" size) for one of its printings, /// or empty if none is available. pub image_uri: String, /// 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, } /// A library row: a card the user owns, with quantity and its deck memberships. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LibraryCard { #[serde(flatten)] pub card: CardRef, pub quantity_owned: i64, /// Names of decks this card is currently assigned to. pub decks: Vec, /// Total copies assigned across all decks. If it exceeds `quantity_owned`, /// the physical copies are over-committed. pub quantity_assigned: i64, /// Free-text physical storage location (e.g. "Box 1"). Meaningful mainly for /// cards not currently in a deck. pub location: String, } /// A card in a deck, whether or not the user owns it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeckEntry { #[serde(flatten)] pub card: CardRef, /// Copies of this card in the deck. pub quantity: i64, pub is_commander: bool, /// Copies owned in the library (0 if the card isn't in the library). pub quantity_owned: i64, } impl LibraryCard { pub fn is_over_assigned(&self) -> bool { self.quantity_assigned > self.quantity_owned } } /// A deck. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Deck { pub id: i64, pub name: String, /// Free-text format, e.g. "Commander", "Modern" (empty if unset). pub format: String, /// The commander card's oracle id (meaningful for Commander/Brawl formats). pub commander_oracle_id: Option, pub notes: String, } /// Aggregate stats about the owned library, for a dashboard-style summary. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct LibraryStats { /// Distinct oracle cards owned (quantity_owned > 0). pub unique_cards: i64, /// Total physical copies owned, summed across all cards. pub total_copies: i64, pub deck_count: i64, /// Copies owned in each color, WUBRG order. A multicolor card's copies /// count toward every color it contains, so these can sum to more than /// `total_copies`. pub color_counts: [i64; 5], /// Copies owned with no color (colors == 0). pub colorless_count: i64, } /// Criteria for searching the owned library. All fields are optional; an empty /// filter matches every owned card. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default)] pub struct LibraryFilter { /// Fuzzy-ish name text (applied as a case-insensitive substring here; the /// fast fuzzy path is [`crate::search`]). pub text: Option, /// Cards whose `colors` exactly equal this set. pub colors_exact: Option, /// Cards whose `colors` include at least these. pub colors_include: Option, /// Commander compatibility: only cards whose color identity is a subset of /// this set (playable under a commander of this identity). pub commander_identity: Option, pub cmc_min: Option, pub cmc_max: Option, /// Card supertype/type, e.g. "Creature", "Instant" (matched against the /// normalized `card_types` table). pub types: Vec, /// Subtype, e.g. "Elf", "Equipment". pub subtypes: Vec, /// Keyword ability, e.g. "Flying", "Deathtouch". pub keywords: Vec, /// Restrict to cards assigned to this deck id. pub in_deck: Option, /// 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, }