Skip to main content

manabrew_engine/card/
card_zone_table.rs

1//! Zone-change aggregation table (Java parity: `CardZoneTable`).
2
3use std::collections::HashMap;
4
5use forge_foundation::ZoneType;
6use serde::{Deserialize, Serialize};
7
8use crate::card::valid_filter;
9use crate::event::{RunParams, ZoneChangeRecord};
10use crate::game::GameState;
11use crate::ids::{CardId, PlayerId};
12use crate::parsing::CompiledSelector;
13use crate::spellability::SpellAbility;
14use crate::trigger::TriggerHandler;
15use crate::trigger::TriggerType;
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct CardZoneTable {
19    data: HashMap<(ZoneType, ZoneType), Vec<CardId>>,
20    created_tokens: Vec<CardId>,
21    first_time_token_creators: Vec<PlayerId>,
22    last_state_battlefield: Vec<CardId>,
23    last_state_graveyard: Vec<CardId>,
24}
25
26impl CardZoneTable {
27    /// Java parity: special put that appends cards to an origin->destination bucket.
28    pub fn put(&mut self, origin: Option<ZoneType>, destination: Option<ZoneType>, card: CardId) {
29        let from = origin.unwrap_or(ZoneType::None);
30        let to = destination.unwrap_or(ZoneType::None);
31        self.data.entry((from, to)).or_default().push(card);
32    }
33
34    pub fn trigger_changes_zone_all(
35        &self,
36        trigger_handler: &mut TriggerHandler,
37        game: &GameState,
38        cause: Option<&SpellAbility>,
39    ) {
40        if !self.created_tokens.is_empty() {
41            trigger_handler.run_trigger(
42                TriggerType::TokenCreatedOnce,
43                RunParams {
44                    cards: Some(self.created_tokens.clone()),
45                    first_time_players: if self.first_time_token_creators.is_empty() {
46                        None
47                    } else {
48                        Some(self.first_time_token_creators.clone())
49                    },
50                    ..Default::default()
51                },
52                false,
53            );
54        }
55        if !self.data.is_empty() {
56            let table = self.with_last_state(game);
57            for &card_id in table.last_state_battlefield() {
58                trigger_handler.register_active_ltb_trigger(game, card_id);
59            }
60            trigger_handler.run_trigger(
61                TriggerType::ChangesZoneAll,
62                RunParams {
63                    cards: Some(table.all_cards()),
64                    zone_changes: Some(table.zone_changes()),
65                    change_zone_table: Some(table),
66                    cause: cause.cloned(),
67                    ..Default::default()
68                },
69                false,
70            );
71        }
72    }
73
74    pub fn filter_cards(
75        &self,
76        game: &GameState,
77        origin: Option<&[ZoneType]>,
78        destination: Option<&[ZoneType]>,
79        valid: Option<&CompiledSelector>,
80        source: CardId,
81        source_controller: PlayerId,
82    ) -> Vec<CardId> {
83        let mut out = Vec::new();
84        for (&(from, to), cards) in &self.data {
85            if let Some(origins) = origin {
86                if !origins.contains(&from) {
87                    continue;
88                }
89            }
90            if let Some(destinations) = destination {
91                if !destinations.contains(&to) {
92                    continue;
93                }
94            }
95            out.extend(cards.iter().copied());
96        }
97        if let Some(filter) = valid {
98            out.retain(|&cid| {
99                valid_filter::matches_valid_card_selector_in_game(
100                    filter,
101                    game.card(cid),
102                    game.card(source),
103                    game,
104                )
105            });
106        }
107        let _ = source_controller;
108        out
109    }
110
111    pub fn all_cards(&self) -> Vec<CardId> {
112        self.data.values().flat_map(|v| v.iter().copied()).collect()
113    }
114
115    pub fn zone_changes(&self) -> Vec<ZoneChangeRecord> {
116        let mut out = Vec::new();
117        for (&(origin, destination), cards) in &self.data {
118            for &card in cards {
119                out.push(ZoneChangeRecord {
120                    origin,
121                    destination,
122                    card,
123                });
124            }
125        }
126        out
127    }
128
129    pub fn add_token(&mut self, card: CardId, owner: PlayerId, first_time: bool) {
130        self.created_tokens.push(card);
131        if first_time && !self.first_time_token_creators.contains(&owner) {
132            self.first_time_token_creators.push(owner);
133        }
134    }
135
136    pub fn last_state_battlefield(&self) -> &[CardId] {
137        &self.last_state_battlefield
138    }
139
140    pub fn last_state_graveyard(&self) -> &[CardId] {
141        &self.last_state_graveyard
142    }
143
144    fn with_last_state(&self, game: &GameState) -> Self {
145        let mut table = self.clone();
146        table.last_state_battlefield = game.pre_sba_battlefield.clone();
147        table.last_state_graveyard = game.cards_in_all_zones(ZoneType::Graveyard).collect();
148        table
149    }
150}