Skip to main content

manabrew_engine/trigger/
trigger_changes_zone.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ability::ability_utils::parse_counter_type;
4use crate::event::RunParams;
5use crate::game::GameState;
6use crate::parsing::compare::compare_expr;
7use crate::parsing::Params;
8use crate::spellability::SpellAbility;
9use crate::trigger::TriggerType;
10
11use super::trigger::TriggerBehavior;
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct TriggerChangesZone;
15
16impl TriggerChangesZone {
17    pub fn parse(_params: &Params) -> Box<dyn TriggerBehavior> {
18        Box::new(Self)
19    }
20}
21
22#[typetag::serde]
23impl TriggerBehavior for TriggerChangesZone {
24    fn trigger_type(&self) -> TriggerType {
25        TriggerType::ChangesZone
26    }
27
28    fn perform_test(
29        &self,
30        trigger: &super::trigger::Trigger,
31        params: &RunParams,
32        game: &GameState,
33    ) -> bool {
34        let host_card = trigger.base.card_trait_base.host_card_id();
35        let host_controller = trigger.base.card_trait_base.host_controller(game);
36        let current_trigger_id = Some(trigger.id);
37        let origin = trigger.ir.origin_zone;
38        let destination = trigger.ir.destination_zone;
39        if !super::trigger::Trigger::matches_zone_filter(&origin, params.origin)
40            || !super::trigger::Trigger::matches_zone_filter(&destination, params.destination)
41        {
42            return false;
43        }
44        if params
45            .origin
46            .is_some_and(|zone| trigger.ir.excluded_origins.contains(&zone))
47        {
48            return false;
49        }
50        if params
51            .destination
52            .is_some_and(|zone| trigger.ir.excluded_destinations.contains(&zone))
53        {
54            return false;
55        }
56        let mut moved_card = params.card;
57        if params.origin == Some(forge_foundation::ZoneType::Battlefield)
58            || (params.origin == Some(forge_foundation::ZoneType::Graveyard)
59                && destination != Some(forge_foundation::ZoneType::Battlefield))
60        {
61            moved_card = params.card_lki.or(params.card);
62        } else if destination == Some(forge_foundation::ZoneType::Battlefield) {
63            if let Some(card) = params.card {
64                let controller = game.card(card).controller;
65                let zone = game.zone(forge_foundation::ZoneType::Battlefield, controller);
66                if let Some((_, latest)) = zone
67                    .cards_added_this_turn
68                    .iter()
69                    .filter(|(_, added)| *added == card)
70                    .max_by(|(_, a), (_, b)| {
71                        crate::card::card_predicates::compare_by_game_timestamp(game, *a, *b)
72                    })
73                {
74                    moved_card = Some(*latest);
75                }
76            }
77        }
78        if params.origin == Some(forge_foundation::ZoneType::Battlefield)
79            && trigger
80                .base
81                .valid_host_zones
82                .as_deref()
83                .is_some_and(|zones| zones.contains(&forge_foundation::ZoneType::Graveyard))
84            && game.card(host_card).zone == forge_foundation::ZoneType::Graveyard
85            && params
86                .change_zone_table
87                .as_ref()
88                .is_some_and(|table| !table.last_state_graveyard().contains(&host_card))
89        {
90            return false;
91        }
92        let valid_card = trigger.ir.valid_card_selector.clone();
93        if !trigger.matches_optional_valid_card_filter(&valid_card, moved_card, game) {
94            return false;
95        }
96        if let Some(filter) = trigger.ir.valid_cause_selector.as_ref() {
97            let cause_matches = params
98                .cause
99                .as_ref()
100                .and_then(|sa| sa.source)
101                .or(params.cause_card)
102                .or(params.causer)
103                .is_some_and(|cause_card| {
104                    trigger.matches_valid_card_filter(filter, cause_card, game)
105                });
106            if !cause_matches {
107                return false;
108            }
109        }
110        if let Some(expected_fizzle) = trigger.ir.fizzle {
111            if params.fizzle != Some(expected_fizzle) {
112                return false;
113            }
114        }
115        if trigger.ir.not_this_ability
116            && params
117                .cause
118                .as_ref()
119                .and_then(|cause| cause.source_trigger_id)
120                .zip(current_trigger_id)
121                .is_some_and(|(source_trigger_id, trigger_id)| source_trigger_id == trigger_id)
122        {
123            return false;
124        }
125        if let Some(cast_expr) = trigger.ir.condition_you_cast_this_turn.as_deref() {
126            let casting_player = params
127                .spell_controller
128                .or(params.player)
129                .unwrap_or(host_controller);
130            if !compare_expr(game.player(casting_player).spells_cast_this_turn, cast_expr) {
131                return false;
132            }
133        }
134        if let Some(check_expr) = trigger.ir.check_on_triggered_card.as_deref() {
135            let moved = game.card(moved_card.unwrap_or(host_card));
136            let mut parts = check_expr.split_whitespace();
137            let lhs = parts.next().unwrap_or("");
138            let rhs = parts.next().unwrap_or("GE1");
139            let actual_value = match lhs {
140                "Count$CardPower" => moved.power(),
141                "Count$CardToughness" => moved.toughness(),
142                _ => {
143                    let Some(counter_name) = lhs.strip_prefix("Count$CardCounters.") else {
144                        return false;
145                    };
146                    moved.counter_count(&parse_counter_type(counter_name))
147                }
148            };
149            if !compare_expr(actual_value, rhs) {
150                return false;
151            }
152        }
153        true
154    }
155
156    fn set_triggering_objects(
157        &self,
158        trigger: &super::trigger::Trigger,
159        sa: &mut SpellAbility,
160        params: &RunParams,
161        _game: &GameState,
162    ) {
163        // Java: if origin == Battlefield, Card = CardLKI, NewCard = Card
164        //        else: copy both Card and CardLKI from runParams
165        if trigger.ir.origin_zone == Some(forge_foundation::ZoneType::Battlefield) {
166            if let Some(card_id) = params.card_lki.or(params.card) {
167                sa.set_triggering_object(crate::ability::AbilityKey::Card, card_id);
168                if let Some(power) = params.lki_power {
169                    sa.set_triggering_object(
170                        crate::ability::AbilityKey::TriggeredCardPower,
171                        power.to_string(),
172                    );
173                }
174                if let Some(toughness) = params.lki_toughness {
175                    sa.set_triggering_object(
176                        crate::ability::AbilityKey::TriggeredCardToughness,
177                        toughness.to_string(),
178                    );
179                }
180            }
181            if let Some(card_id) = params.card {
182                sa.set_triggering_object(crate::ability::AbilityKey::NewCard, card_id);
183            }
184        } else {
185            if let Some(card_id) = params.card {
186                sa.set_triggering_object(crate::ability::AbilityKey::Card, card_id);
187            }
188            if let Some(card_lki) = params.card_lki {
189                sa.set_triggering_object(crate::ability::AbilityKey::CardLKI, card_lki);
190            }
191        }
192    }
193
194    fn get_important_stack_objects(
195        &self,
196        _trigger: &super::trigger::Trigger,
197        sa: &SpellAbility,
198    ) -> String {
199        format!(
200            "Zone Changer: {}",
201            sa.get_triggering_object(crate::ability::AbilityKey::Card)
202                .unwrap_or_default()
203        )
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
210
211    use super::*;
212    use crate::card::Card;
213    use crate::event::RunParams;
214    use crate::game::GameState;
215    use crate::ids::{CardId, PlayerId};
216
217    #[test]
218    fn changes_zone_uses_lki_card_for_leaves_battlefield_valid_card() {
219        let mut game = GameState::new(&["A", "B"], 20);
220        let p0 = PlayerId(0);
221
222        let mut card = Card::new(
223            CardId(0),
224            "Test".to_string(),
225            p0,
226            CardTypeLine::parse("Creature"),
227            ManaCost::parse(""),
228            ColorSet::COLORLESS,
229            Some(2),
230            Some(2),
231            vec![],
232            vec![],
233        );
234        card.set_zone(ZoneType::Graveyard);
235        let cid = game.create_card(card);
236
237        let trigger = TriggerChangesZone;
238
239        let params = RunParams {
240            card: None,
241            card_lki: Some(cid),
242            origin: Some(ZoneType::Battlefield),
243            destination: Some(ZoneType::Graveyard),
244            ..Default::default()
245        };
246
247        // TODO: update test to construct a full Trigger with host card to call perform_test
248        let _ = (trigger, params, game, cid, p0);
249    }
250}