Files
mtgc/crates/mtgc-core/src/model.rs

188 lines
6.5 KiB
Rust
Raw Normal View History

//! 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<I, S>(letters: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
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<String>,
/// 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<String>,
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<String>,
/// Cards whose `colors` exactly equal this set.
pub colors_exact: Option<ColorSet>,
/// Cards whose `colors` include at least these.
pub colors_include: Option<ColorSet>,
/// Commander compatibility: only cards whose color identity is a subset of
/// this set (playable under a commander of this identity).
pub commander_identity: Option<ColorSet>,
pub cmc_min: Option<f64>,
pub cmc_max: Option<f64>,
/// Card supertype/type, e.g. "Creature", "Instant" (matched against the
/// normalized `card_types` table).
pub types: Vec<String>,
/// Subtype, e.g. "Elf", "Equipment".
pub subtypes: Vec<String>,
/// Keyword ability, e.g. "Flying", "Deathtouch".
pub keywords: Vec<String>,
/// Restrict to cards assigned to this deck id.
pub in_deck: Option<i64>,
/// 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>,
}