manabrew_engine/ability/effects/
condition.rs1use forge_foundation::ZoneType;
4
5use crate::game::GameState;
6use crate::ids::{CardId, PlayerId};
7use crate::parsing::compare::{compare_expr, CompareExpr};
8use crate::spellability::SpellAbility;
9
10use super::helpers::matches_valid_cards_for_sa;
11
12pub(super) fn check_condition(game: &GameState, sa: &SpellAbility) -> bool {
14 let activator = sa.activating_player;
15
16 if sa.ir.condition_hellbent && !crate::player::has_hellbent(game, activator) {
18 return false;
19 }
20 if sa.ir.condition_threshold && !crate::player::has_threshold(game, activator) {
21 return false;
22 }
23 if sa.ir.condition_metalcraft && !crate::player::has_metalcraft(game, activator) {
24 return false;
25 }
26 if sa.ir.condition_delirium && !crate::player::has_delirium(game, activator) {
27 return false;
28 }
29 if sa.ir.condition_revolt && !crate::player::has_revolt(game, activator) {
30 return false;
31 }
32 if sa.ir.condition_desert && !crate::player::has_desert(game, activator) {
33 return false;
34 }
35 if sa.ir.condition_blessing && !crate::player::has_blessing(game, activator) {
36 return false;
37 }
38
39 if sa.ir.condition_kicked && !sa.kicked {
41 return false;
42 }
43 if sa.ir.condition_optional_paid && !sa.optional_generic_cost_paid {
44 return false;
45 }
46 if sa.ir.condition_optional_not_paid && sa.optional_generic_cost_paid {
47 return false;
48 }
49
50 if let Some(raw) = sa.ir.condition_player_turn.as_deref() {
52 let expect_self_turn = !raw.eq_ignore_ascii_case("False");
53 let is_self_turn = game.turn.active_player == activator;
54 if expect_self_turn != is_self_turn {
55 return false;
56 }
57 }
58 if sa.ir.condition_opponent_turn && game.turn.active_player == activator {
59 return false;
60 }
61
62 if let Some(raw) = sa.ir.condition_cards_in_hand.as_deref() {
64 let size = game
65 .cards_in_zone(forge_foundation::ZoneType::Hand, activator)
66 .len() as i32;
67 if let Ok(n) = raw.parse::<i32>() {
68 if size != n {
69 return false;
70 }
71 } else if !compare_expr(size, raw) {
72 return false;
73 }
74 }
75
76 if let Some(phases) = sa.ir.condition_phases.as_deref() {
78 let current = game.turn.phase;
79 let ok = phases
80 .split(',')
81 .map(str::trim)
82 .filter_map(forge_foundation::PhaseType::from_script_name)
83 .any(|p| p == current);
84 if !ok {
85 return false;
86 }
87 }
88
89 if let Some(cmp) = sa.ir.condition_life_compare.as_deref() {
91 if !compare_expr(game.player(activator).life, cmp) {
92 return false;
93 }
94 }
95
96 if let Some(cond) = sa.ir.condition.as_deref() {
98 if cond == "Kicked" {
99 return sa.kicked;
100 }
101 }
102 if let Some(cond) = sa.ir.condition_check_svar.as_deref() {
104 if cond == "Kicked" || cond == "X:Kicked" {
105 return sa.kicked;
106 }
107 let compare = sa.ir.condition_svar_compare.as_deref().unwrap_or("GE1");
108 let Some(source_id) = sa.source else {
109 return false;
110 };
111 let Some(expr) = game.card(source_id).get_s_var(cond) else {
112 return false;
113 };
114
115 let value = if let Some(valid_filter) = expr.strip_prefix("Imprinted$Valid ") {
116 let imprinted = game.card(source_id).imprinted_cards.clone();
117 if valid_filter.eq_ignore_ascii_case("Card.sharesNameWith Remembered") {
118 let remembered_names: std::collections::HashSet<String> = game
119 .card(source_id)
120 .remembered_cards
121 .iter()
122 .map(|&cid| game.card(cid).card_name.clone())
123 .collect();
124 imprinted
125 .into_iter()
126 .filter(|&cid| remembered_names.contains(&game.card(cid).card_name))
127 .count() as i32
128 } else {
129 imprinted
130 .into_iter()
131 .filter(|&cid| {
132 matches_valid_cards_for_sa(game, sa, game.card(cid), None, valid_filter)
133 })
134 .count() as i32
135 }
136 } else {
137 crate::svar::resolve_svar_expression(expr, game, source_id, sa.activating_player, sa)
138 };
139 return compare_with_svar_threshold(value, compare, game, source_id, sa);
140 }
141 true
142}
143
144fn compare_with_svar_threshold(
150 value: i32,
151 compare: &str,
152 game: &GameState,
153 source_id: CardId,
154 sa: &SpellAbility,
155) -> bool {
156 if let Some(parsed) = CompareExpr::parse(compare) {
157 return parsed.evaluate(value);
158 }
159 let Some((op_str, rhs_name)) = split_compare_prefix(compare) else {
160 return true;
161 };
162 let Some(rhs_expr) = game.card(source_id).get_s_var(rhs_name) else {
163 return true;
164 };
165 let rhs_value =
166 crate::svar::resolve_svar_expression(rhs_expr, game, source_id, sa.activating_player, sa);
167 compare_expr(value, &format!("{op_str}{rhs_value}"))
168}
169
170fn split_compare_prefix(expr: &str) -> Option<(&'static str, &str)> {
171 for prefix in ["GE", "GT", "LE", "LT", "NE", "EQ"] {
172 if let Some(rest) = expr.strip_prefix(prefix) {
173 return Some((prefix, rest));
174 }
175 }
176 None
177}
178
179pub(super) fn check_condition_present(
185 game: &GameState,
186 sa: &SpellAbility,
187 player: PlayerId,
188 source_id: CardId,
189) -> bool {
190 let condition = match sa.ir.condition_present.as_deref() {
191 Some(c) => c,
192 None => return true, };
194
195 let alternatives: Vec<&str> = condition.split(',').map(|s| s.trim()).collect();
197
198 if let Some(cond_defined) = sa.ir.condition_defined.as_ref() {
200 let defined_cards: Vec<CardId> = match cond_defined.refs.first() {
201 Some(crate::ability::ability_ir::DefinedRef::Targeted) => {
202 sa.target_chosen.target_card.into_iter().collect()
203 }
204 Some(crate::ability::ability_ir::DefinedRef::SelfCard) => {
205 sa.source.into_iter().collect()
206 }
207 Some(crate::ability::ability_ir::DefinedRef::Remembered) => sa
208 .source
209 .map(|sid| game.card(sid).remembered_cards.clone())
210 .unwrap_or_default(),
211 Some(other) => match other.as_legacy_str() {
212 "Targeted" => sa.target_chosen.target_card.into_iter().collect(),
213 "Self" => sa.source.into_iter().collect(),
214 "Remembered" => sa
215 .source
216 .map(|sid| game.card(sid).remembered_cards.clone())
217 .unwrap_or_default(),
218 _ => Vec::new(),
219 },
220 None => Vec::new(),
221 };
222
223 let count = defined_cards
226 .iter()
227 .filter(|&&cid| {
228 matches_condition_filter_no_self_exclude(
229 game,
230 cid,
231 source_id,
232 player,
233 &alternatives,
234 )
235 })
236 .count() as i32;
237
238 return if let Some(compare) = sa.ir.condition_compare.as_deref() {
239 compare_expr(count, compare)
240 } else {
241 count > 0
242 };
243 }
244
245 let zone = sa.ir.condition_zone.unwrap_or(ZoneType::Battlefield);
246
247 let cards: Vec<CardId> = game
248 .players
249 .iter()
250 .flat_map(|p| game.cards_in_zone(zone, p.id).iter().copied())
251 .collect();
252 let count = cards
253 .iter()
254 .filter(|&&cid| {
255 matches_condition_filter_no_self_exclude(game, cid, source_id, player, &alternatives)
256 })
257 .count() as i32;
258
259 if let Some(compare) = sa.ir.condition_compare.as_deref() {
261 compare_expr(count, compare)
262 } else {
263 count > 0
264 }
265}
266
267fn matches_condition_filter_no_self_exclude(
272 game: &GameState,
273 cid: CardId,
274 source_id: CardId,
275 player: PlayerId,
276 alternatives: &[&str],
277) -> bool {
278 let card = game.card(cid);
279 let source = game.card(source_id);
280 alternatives
281 .iter()
282 .any(|alt| crate::card::valid_filter::matches_valid(alt, Some(card), None, source, player))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::card::Card;
289 use crate::game::GameState;
290 use crate::ids::{CardId, PlayerId};
291 use crate::spellability::SpellAbility;
292 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
293
294 #[test]
295 fn condition_present_targeted_nonbasic_matches_destroyed_land() {
296 let player = PlayerId(0);
297 let mut game = GameState::new(&["P1"], 20);
298 let spell_source = game.create_card(Card::new(
299 CardId(0),
300 "Choking Sands".to_string(),
301 player,
302 CardTypeLine::parse("Sorcery"),
303 ManaCost::parse("1 B B"),
304 ColorSet::BLACK,
305 None,
306 None,
307 vec![],
308 vec![],
309 ));
310 let target = game.create_card(Card::new(
311 CardId(0),
312 "Cliffgate".to_string(),
313 player,
314 CardTypeLine::parse("Land - Gate"),
315 ManaCost::parse(""),
316 ColorSet::COLORLESS,
317 None,
318 None,
319 vec![],
320 vec![],
321 ));
322
323 let mut sa = SpellAbility::new_simple(
324 Some(spell_source),
325 player,
326 "DB$ DealDamage | ConditionDefined$ Targeted | ConditionPresent$ Land.Basic | ConditionCompare$ EQ0",
327 );
328 sa.target_chosen.target_card = Some(target);
329
330 assert!(check_condition_present(&game, &sa, player, spell_source));
331 }
332}