Skip to main content

manabrew_engine/trigger/
trigger_changes_zone_all.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ability::AbilityKey;
4use crate::event::{AbilityValue, RunParams};
5use crate::game::GameState;
6use crate::ids::CardId;
7use crate::parsing::compare::compare_expr;
8use crate::parsing::{keys, Params};
9use crate::spellability::SpellAbility;
10use crate::trigger::TriggerType;
11
12use super::trigger::TriggerBehavior;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TriggerChangesZoneAll {
16    pub origin: Option<forge_foundation::ZoneType>,
17    pub destination: Option<forge_foundation::ZoneType>,
18    pub valid_card: Option<crate::parsing::CompiledSelector>,
19    pub valid_cause: Option<crate::parsing::CompiledSelector>,
20    pub first_time_only: bool,
21    pub valid_amount: Option<String>,
22}
23
24impl TriggerChangesZoneAll {
25    pub fn parse(params: &Params) -> Box<dyn TriggerBehavior> {
26        let origin = params.zone_type(keys::ORIGIN);
27        let destination = params.zone_type(keys::DESTINATION);
28        let valid_card = params.selector_cloned_any(&[keys::VALID_CARDS, keys::VALID_CARD]);
29        let valid_cause = params.selector_cloned(keys::VALID_CAUSE);
30        let first_time_only = params.has("FirstTime");
31        let valid_amount = params.get_cloned("ValidAmount");
32        Box::new(Self {
33            origin,
34            destination,
35            valid_card,
36            valid_cause,
37            first_time_only,
38            valid_amount,
39        })
40    }
41}
42
43#[typetag::serde]
44impl TriggerBehavior for TriggerChangesZoneAll {
45    fn trigger_type(&self) -> TriggerType {
46        TriggerType::ChangesZoneAll
47    }
48
49    fn perform_test(
50        &self,
51        trigger: &super::trigger::Trigger,
52        params: &RunParams,
53        game: &GameState,
54    ) -> bool {
55        let host_card = trigger.base.card_trait_base.host_card_id();
56        let host_controller = trigger.base.card_trait_base.host_controller(game);
57        let table = match params.get_value(AbilityKey::Cards) {
58            Some(AbilityValue::CardZoneTable(table)) => Some(table),
59            _ => None,
60        };
61
62        if let Some(filter) = &self.valid_cause {
63            let Some(cause_card) = (match params.get_value(AbilityKey::Cause) {
64                Some(AbilityValue::SpellAbility(sa)) => sa.source,
65                Some(AbilityValue::Card(card)) => Some(card),
66                _ => None,
67            }) else {
68                return false;
69            };
70            if !trigger.matches_valid_card_filter(filter, cause_card, game) {
71                return false;
72            }
73        }
74
75        let matching: Vec<CardId> = if let Some(table) = table.as_ref() {
76            let origins = self.origin.map(|zone| vec![zone]);
77            let destinations = self.destination.map(|zone| vec![zone]);
78            table.filter_cards(
79                game,
80                origins.as_deref(),
81                destinations.as_deref(),
82                self.valid_card.as_ref(),
83                host_card,
84                host_controller,
85            )
86        } else {
87            let Some(zone_changes) = params.zone_changes.as_ref() else {
88                return false;
89            };
90            zone_changes
91                .iter()
92                .filter(|zc| self.origin.is_none_or(|expected| zc.origin == expected))
93                .filter(|zc| {
94                    self.destination
95                        .is_none_or(|expected| zc.destination == expected)
96                })
97                .filter_map(|zc| {
98                    if trigger.matches_optional_valid_card_filter(
99                        &self.valid_card,
100                        Some(zc.card),
101                        game,
102                    ) {
103                        Some(zc.card)
104                    } else {
105                        None
106                    }
107                })
108                .collect()
109        };
110
111        if matching.is_empty() {
112            return false;
113        }
114
115        if self.first_time_only {
116            if let Some(table) = table.as_ref() {
117                let seen_before = table
118                    .filter_cards(
119                        game,
120                        self.origin.map(|zone| vec![zone]).as_deref(),
121                        self.destination.map(|zone| vec![zone]).as_deref(),
122                        self.valid_card.as_ref(),
123                        host_card,
124                        host_controller,
125                    )
126                    .into_iter()
127                    .filter(|card| !matching.contains(card))
128                    .count();
129                if seen_before > 0 {
130                    return false;
131                }
132            } else {
133                for &card_id in &matching {
134                    let card = game.card(card_id);
135                    let zone_owner = if card.zone == forge_foundation::ZoneType::Battlefield {
136                        card.controller
137                    } else {
138                        card.owner
139                    };
140                    let zone = game.zone(card.zone, zone_owner);
141                    let seen_before = zone
142                        .cards_added_this_turn
143                        .iter()
144                        .filter(|(_, seen_card)| !matching.contains(seen_card))
145                        .filter(|(seen_origin, seen_card)| {
146                            self.origin.is_none_or(|expected| *seen_origin == expected)
147                                && trigger.matches_optional_valid_card_filter(
148                                    &self.valid_card,
149                                    Some(*seen_card),
150                                    game,
151                                )
152                        })
153                        .count();
154                    if seen_before > 0 {
155                        return false;
156                    }
157                }
158            }
159        }
160
161        if let Some(amount_filter) = &self.valid_amount {
162            return compare_expr(matching.len() as i32, amount_filter);
163        }
164
165        true
166    }
167
168    fn set_triggering_objects(
169        &self,
170        _trigger: &super::trigger::Trigger,
171        sa: &mut SpellAbility,
172        params: &RunParams,
173        _game: &GameState,
174    ) {
175        // TODO: Java calls this.filterCards(table) to filter by ValidCards param,
176        // but we don't have access to the trigger params here. Passing through all cards.
177        if let Some(cards) = params.cards.as_ref() {
178            let csv = cards
179                .iter()
180                .map(|c| c.0.to_string())
181                .collect::<Vec<_>>()
182                .join(",");
183            sa.set_triggering_object(crate::ability::AbilityKey::Cards, &csv);
184            sa.set_triggering_object(crate::ability::AbilityKey::Amount, cards.len().to_string());
185            // Also set trigger_remembered_amount so TriggerCount$Amount SVars
186            // (e.g. Woodland Champion's CounterNum$ X where X = TriggerCount$Amount)
187            // resolve to the correct count instead of defaulting to 1.
188            sa.trigger_remembered_amount = cards.len() as i32;
189        }
190        // TODO: Java also sets Cause from runParams via
191        // sa.setTriggeringObjectsFrom(runParams, AbilityKey.Cause)
192        // Skipping Cause for now since SpellAbility is complex and stored as object in Java
193    }
194
195    fn origin_zone(&self) -> Option<forge_foundation::ZoneType> {
196        self.origin
197    }
198
199    fn destination_zone(&self) -> Option<forge_foundation::ZoneType> {
200        self.destination
201    }
202
203    fn get_important_stack_objects(
204        &self,
205        _trigger: &super::trigger::Trigger,
206        sa: &SpellAbility,
207    ) -> String {
208        format!(
209            "Amount: {}",
210            sa.trigger_objects
211                .get(&crate::ability::AbilityKey::Amount)
212                .cloned()
213                .unwrap_or_default()
214        )
215    }
216}