manabrew_engine/trigger/
trigger_changes_zone.rs1use 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 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 let _ = (trigger, params, game, cid, p0);
249 }
250}