use crate::models::card::CardDefinition; use anyhow::{Context, Result}; use std::collections::HashMap; use std::path::Path; /// In-memory card registry loaded from data/cards/*.json pub struct CardDb { pub cards: HashMap, } impl CardDb { pub fn load(data_dir: &str) -> Result { let cards_dir = Path::new(data_dir).join("cards"); let mut cards = HashMap::new(); if !cards_dir.exists() { tracing::warn!("Card data directory not found: {:?}", cards_dir); return Ok(Self { cards }); } for entry in std::fs::read_dir(&cards_dir) .with_context(|| format!("reading cards dir {:?}", cards_dir))? { let entry = entry?; let path = entry.path(); if path.extension().map(|e| e == "json").unwrap_or(false) { let content = std::fs::read_to_string(&path) .with_context(|| format!("reading {:?}", path))?; let batch: Vec = serde_json::from_str(&content) .with_context(|| format!("parsing {:?}", path))?; for card in batch { cards.insert(card.id.clone(), card); } } } tracing::info!("Loaded {} card definitions", cards.len()); Ok(Self { cards }) } /// Merge a game's **opt-in development content pack** from /// `{data_dir}/games/{game}/dev/cards.json` (a single `CardDefinition[]`). /// This is NOT read by [`CardDb::load`]; it is loaded only when a game is /// explicitly named in `Config::dev_content_games`, so default content stays /// untouched. Returns the number of definitions merged. A missing file is an /// error (opt-in means the pack is expected to exist). pub fn load_game_dev(&mut self, data_dir: &str, game: &str) -> Result { let path = Path::new(data_dir) .join("games") .join(game) .join("dev") .join("cards.json"); let content = std::fs::read_to_string(&path) .with_context(|| format!("reading dev content pack {path:?}"))?; let batch: Vec = serde_json::from_str(&content).with_context(|| format!("parsing {path:?}"))?; let n = batch.len(); for card in batch { self.cards.insert(card.id.clone(), card); } tracing::info!("Loaded {} dev card definitions for game '{}'", n, game); Ok(n) } pub fn get(&self, id: &str) -> Option<&CardDefinition> { self.cards.get(id) } pub fn all(&self) -> Vec<&CardDefinition> { self.cards.values().collect() } pub fn by_min_overall(&self, min: u8) -> Vec<&CardDefinition> { self.cards.values().filter(|c| c.overall >= min).collect() } }