manabrew_engine/trigger/
trigger_changes_zone_all.rs1use 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 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 sa.trigger_remembered_amount = cards.len() as i32;
189 }
190 }
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}