weiss_core/db/
store.rs

1use anyhow::{Context, Result};
2
3use super::ability::{
4    ability_sort_key, compile_effects_from_def, compile_effects_from_template, AbilitySpec,
5    AbilityTemplate,
6};
7use super::card::CardStatic;
8use super::types::{CardColor, CardId, CardType};
9
10/// Loaded card database with cached per-id lookups and compiled abilities.
11#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
12#[serde(try_from = "CardDbRaw", into = "CardDbRaw")]
13pub struct CardDb {
14    /// Canonical list of static card definitions.
15    pub cards: Vec<CardStatic>,
16    #[serde(skip)]
17    index: Vec<usize>,
18    #[serde(skip)]
19    valid_by_id: Vec<bool>,
20    #[serde(skip)]
21    power_by_id: Vec<i32>,
22    #[serde(skip)]
23    soul_by_id: Vec<u8>,
24    #[serde(skip)]
25    level_by_id: Vec<u8>,
26    #[serde(skip)]
27    cost_by_id: Vec<u8>,
28    #[serde(skip)]
29    color_by_id: Vec<CardColor>,
30    #[serde(skip)]
31    card_type_by_id: Vec<CardType>,
32    #[serde(skip)]
33    ability_specs: Vec<Vec<AbilitySpec>>,
34    #[serde(skip)]
35    compiled_ability_effects: Vec<Vec<Vec<crate::effects::EffectSpec>>>,
36}
37
38#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
39struct CardDbRaw {
40    cards: Vec<CardStatic>,
41}
42
43impl TryFrom<CardDbRaw> for CardDb {
44    type Error = anyhow::Error;
45
46    fn try_from(raw: CardDbRaw) -> Result<Self> {
47        CardDb::new(raw.cards)
48    }
49}
50
51impl From<CardDb> for CardDbRaw {
52    fn from(db: CardDb) -> Self {
53        Self { cards: db.cards }
54    }
55}
56
57impl CardDb {
58    /// Build a new database from raw card definitions.
59    ///
60    /// Validates ids, canonicalizes templates, and builds per-id caches.
61    pub fn new(cards: Vec<CardStatic>) -> Result<Self> {
62        let mut db = Self {
63            cards,
64            index: Vec::new(),
65            valid_by_id: Vec::new(),
66            power_by_id: Vec::new(),
67            soul_by_id: Vec::new(),
68            level_by_id: Vec::new(),
69            cost_by_id: Vec::new(),
70            color_by_id: Vec::new(),
71            card_type_by_id: Vec::new(),
72            ability_specs: Vec::new(),
73            compiled_ability_effects: Vec::new(),
74        };
75        db.build_index()?;
76        Ok(db)
77    }
78
79    /// Look up a card by id. Returns `None` for invalid or zero ids.
80    pub fn get(&self, id: CardId) -> Option<&CardStatic> {
81        if id == 0 {
82            return None;
83        }
84        let idx = *self.index.get(id as usize)?;
85        if idx == usize::MAX {
86            return None;
87        }
88        self.cards.get(idx)
89    }
90
91    #[inline(always)]
92    /// Maximum card id present in the index.
93    pub fn max_card_id(&self) -> CardId {
94        self.index.len().saturating_sub(1).try_into().unwrap_or(0)
95    }
96
97    #[inline(always)]
98    /// Whether an id exists in this database.
99    pub fn is_valid_id(&self, id: CardId) -> bool {
100        self.valid_by_id.get(id as usize).copied().unwrap_or(false)
101    }
102
103    #[inline(always)]
104    /// Cached power value for a card id, or 0 if invalid.
105    pub fn power_by_id(&self, id: CardId) -> i32 {
106        self.power_by_id.get(id as usize).copied().unwrap_or(0)
107    }
108
109    #[inline(always)]
110    /// Cached soul value for a card id, or 0 if invalid.
111    pub fn soul_by_id(&self, id: CardId) -> u8 {
112        self.soul_by_id.get(id as usize).copied().unwrap_or(0)
113    }
114
115    #[inline(always)]
116    /// Cached level value for a card id, or 0 if invalid.
117    pub fn level_by_id(&self, id: CardId) -> u8 {
118        self.level_by_id.get(id as usize).copied().unwrap_or(0)
119    }
120
121    #[inline(always)]
122    /// Cached cost value for a card id, or 0 if invalid.
123    pub fn cost_by_id(&self, id: CardId) -> u8 {
124        self.cost_by_id.get(id as usize).copied().unwrap_or(0)
125    }
126
127    #[inline(always)]
128    /// Cached color value for a card id, defaulting to red.
129    pub fn color_by_id(&self, id: CardId) -> CardColor {
130        self.color_by_id
131            .get(id as usize)
132            .copied()
133            .unwrap_or(CardColor::Red)
134    }
135
136    #[inline(always)]
137    /// Cached card type for a card id, defaulting to character.
138    pub fn card_type_by_id(&self, id: CardId) -> CardType {
139        self.card_type_by_id
140            .get(id as usize)
141            .copied()
142            .unwrap_or(CardType::Character)
143    }
144
145    pub(super) fn build_index(&mut self) -> Result<()> {
146        let mut max_id: usize = 0;
147        for card in &mut self.cards {
148            if card.id == 0 {
149                anyhow::bail!("CardId 0 is reserved for empty and cannot appear in the db");
150            }
151            if card.counter_timing
152                && !matches!(card.card_type, CardType::Event | CardType::Character)
153            {
154                eprintln!(
155                    "CardId {} has counter timing but card_type {:?} is not eligible; disabling counter timing",
156                    card.id, card.card_type
157                );
158                card.counter_timing = false;
159            }
160            for def in &card.ability_defs {
161                def.validate()
162                    .with_context(|| format!("CardId {} AbilityDef invalid", card.id))?;
163            }
164            max_id = max_id.max(card.id as usize);
165        }
166        let mut index = vec![usize::MAX; max_id + 1];
167        let mut valid_by_id = vec![false; max_id + 1];
168        let mut power_by_id = vec![0i32; max_id + 1];
169        let mut soul_by_id = vec![0u8; max_id + 1];
170        let mut level_by_id = vec![0u8; max_id + 1];
171        let mut cost_by_id = vec![0u8; max_id + 1];
172        let mut color_by_id = vec![CardColor::Red; max_id + 1];
173        let mut card_type_by_id = vec![CardType::Character; max_id + 1];
174        for (i, card) in self.cards.iter().enumerate() {
175            let id = card.id as usize;
176            if index[id] != usize::MAX {
177                anyhow::bail!("Duplicate CardId {id}");
178            }
179            index[id] = i;
180            valid_by_id[id] = true;
181            power_by_id[id] = card.power;
182            soul_by_id[id] = card.soul;
183            level_by_id[id] = card.level;
184            cost_by_id[id] = card.cost;
185            color_by_id[id] = card.color;
186            card_type_by_id[id] = card.card_type;
187        }
188        self.index = index;
189        self.valid_by_id = valid_by_id;
190        self.power_by_id = power_by_id;
191        self.soul_by_id = soul_by_id;
192        self.level_by_id = level_by_id;
193        self.cost_by_id = cost_by_id;
194        self.color_by_id = color_by_id;
195        self.card_type_by_id = card_type_by_id;
196        self.build_ability_specs()?;
197        self.build_compiled_abilities()?;
198        Ok(())
199    }
200
201    fn build_ability_specs(&mut self) -> Result<()> {
202        let mut specs_list: Vec<Vec<AbilitySpec>> = Vec::with_capacity(self.cards.len());
203        for card in &self.cards {
204            for template in &card.abilities {
205                if matches!(
206                    template,
207                    AbilityTemplate::ActivatedPlaceholder | AbilityTemplate::Unsupported { .. }
208                ) {
209                    anyhow::bail!(
210                        "CardId {} uses unsupported ability template; update card db",
211                        card.id
212                    );
213                }
214            }
215            let mut specs: Vec<AbilitySpec> = card
216                .abilities
217                .iter()
218                .map(AbilitySpec::from_template)
219                .collect();
220            for def in &card.ability_defs {
221                specs.push(AbilitySpec::from_template(&AbilityTemplate::AbilityDef(
222                    def.clone(),
223                )));
224            }
225            specs.sort_by_cached_key(ability_sort_key);
226            specs_list.push(specs);
227        }
228        self.ability_specs = specs_list;
229        Ok(())
230    }
231
232    fn build_compiled_abilities(&mut self) -> Result<()> {
233        let mut compiled: Vec<Vec<Vec<crate::effects::EffectSpec>>> =
234            Vec::with_capacity(self.cards.len());
235        for card in &self.cards {
236            let specs = self.iter_card_abilities_in_canonical_order(card.id);
237            let mut per_ability: Vec<Vec<crate::effects::EffectSpec>> =
238                Vec::with_capacity(specs.len());
239            for (ability_index, spec) in specs.iter().enumerate() {
240                let idx = u8::try_from(ability_index).map_err(|_| {
241                    anyhow::anyhow!(
242                        "Ability index out of range for card {}: {}",
243                        card.id,
244                        ability_index
245                    )
246                })?;
247                let effects = match &spec.template {
248                    AbilityTemplate::AbilityDef(def) => compile_effects_from_def(card.id, idx, def),
249                    AbilityTemplate::Vanilla | AbilityTemplate::Unsupported { .. } => Vec::new(),
250                    _ => compile_effects_from_template(card.id, idx, &spec.template),
251                };
252                per_ability.push(effects);
253            }
254            compiled.push(per_ability);
255        }
256        self.compiled_ability_effects = compiled;
257        Ok(())
258    }
259
260    /// Abilities in canonical order for a given card id.
261    pub fn iter_card_abilities_in_canonical_order(&self, card_id: CardId) -> &[AbilitySpec] {
262        let idx = match self.index.get(card_id as usize) {
263            Some(idx) => *idx,
264            None => return &[],
265        };
266        if idx == usize::MAX {
267            return &[];
268        }
269        self.ability_specs
270            .get(idx)
271            .map(|v| v.as_slice())
272            .unwrap_or(&[])
273    }
274
275    /// Compiled effects for a specific ability on a card.
276    pub fn compiled_effects_for_ability(
277        &self,
278        card_id: CardId,
279        ability_index: usize,
280    ) -> &[crate::effects::EffectSpec] {
281        let idx = match self.index.get(card_id as usize) {
282            Some(idx) => *idx,
283            None => return &[],
284        };
285        if idx == usize::MAX {
286            return &[];
287        }
288        self.compiled_ability_effects
289            .get(idx)
290            .and_then(|per_ability| per_ability.get(ability_index))
291            .map(|v| v.as_slice())
292            .unwrap_or(&[])
293    }
294
295    /// Flattened list of all compiled effects for a card.
296    pub fn compiled_effects_flat(&self, card_id: CardId) -> Vec<crate::effects::EffectSpec> {
297        let idx = match self.index.get(card_id as usize) {
298            Some(idx) => *idx,
299            None => return Vec::new(),
300        };
301        if idx == usize::MAX {
302            return Vec::new();
303        }
304        let Some(per_ability) = self.compiled_ability_effects.get(idx) else {
305            return Vec::new();
306        };
307        let mut out = Vec::new();
308        for effects in per_ability {
309            out.extend(effects.iter().cloned());
310        }
311        out
312    }
313}