1use std::collections::HashMap;
14
15use forge_foundation::ColorSet;
16use forge_foundation::ZoneType;
17use serde::{Deserialize, Serialize};
18use strum_macros::EnumString;
19
20use super::static_ability_mode;
21use crate::card::valid_filter::CardTraitRequirementsIr;
22use crate::card::Card;
23use crate::card::CounterType;
24use crate::card_trait_base::{CardTrait, CardTraitBase, CardTraitIrOwner};
25use crate::core::HasSVars;
26use crate::game::GameState;
27use crate::ids::{CardId, PlayerId};
28use crate::parsing::keys;
29use crate::parsing::{CompiledSelector, Params};
30
31const STATIC_ZONE_KEYS: &[&str] = &[keys::ACTIVE_ZONES, keys::EFFECT_ZONE];
32
33const STATIC_CONDITION_KEYS: &[&str] = &[
34 keys::PHASES,
35 keys::CONDITION,
36 keys::PLAYER_TURN,
37 "TopCardOfLibraryIs",
38 "ClassLevel",
39 "CheckSecondSVar",
40 "CheckThirdSVar",
41 "CheckFourthSVar",
42];
43
44#[derive(
51 Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, EnumString,
52)]
53#[strum(ascii_case_insensitive)]
54pub enum StaticMode {
55 Continuous,
59
60 CantAttack,
62
63 CantBlock,
65
66 ETBTapped,
69
70 CantBeCast,
72 CantBeActivated,
74 CantPlayLand,
76
77 ReduceCost,
79
80 RaiseCost,
82
83 SetCost,
85 CantTarget,
86 CantAttach,
87 MustAttack,
88 MustBlock,
89 Panharmonicon,
90 CantGainLosePayLife,
91 CantDraw,
92 CantExile,
93 CantSacrifice,
94 CantRegenerate,
95 DisableTriggers,
96 CantPutCounter,
97 CastWithFlash,
98 BlockRestrict,
99 AttackRestrict,
100 CanAttackDefender,
101 IgnoreHexproof,
102 IgnoreShroud,
103 IgnoreLegendRule,
104 MustTarget,
105 AssignCombatDamageAsUnblocked,
106 AssignNoCombatDamage,
107 CombatDamageToughness,
108 NoCleanupDamage,
109 InfectDamage,
110 WitherDamage,
111 ColorlessDamageSource,
112 CountersRemain,
113 MaxCounter,
114 CantAttackUnless,
116 OptionalAttackCost,
118 OptionalCost,
120 AttackRequirement,
122 PlayerMustAttack,
124 CantBlockUnless,
126 CantBlockBy,
129 ManaConvert,
131 UnspentMana,
133 ManaBurn,
135 ActivateAbilityAsIfHaste,
136 CanAdapt,
137 AlternativeCost,
138 CantBeCopied,
139 CantBeSuspected,
140 CantBecomeMonarch,
141 CantChangeDayTime,
142 CantCrew,
143 CantDiscard,
144 CantPhaseIn,
145 CantPhaseOut,
146 CantTransform,
147 CantVenture,
148 Devotion,
149 CanExhaust,
150 FlipCoinMod,
151 GainLifeRadiation,
152 IgnoreLandwalk,
153 NumLoyaltyAct,
154 PlotZone,
155 SurveilNum,
156 TapPowerValue,
157 TurnReversed,
158 PhaseReversed,
159 UntapOtherPlayer,
160 CanBlockIfReach,
161 BlockTapped,
162 CanAttackIfHaste,
163 MinMaxBlocker,
164 AttackVigilance,
165 CantPreventDamage,
166 CantGainLife,
167 CantLoseLife,
168 CantPayLife,
169 CantChangeLife,
170
171 #[strum(default)]
173 Other(String),
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
186pub enum Layer {
187 Control = 2,
189 Text = 3,
191 Type = 4,
193 Color = 5,
195 Ability = 6,
197 Characteristic = 70,
199 SetPT = 71,
201 ModifyPT = 72,
203 Rules = 80,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct StaticAbility {
215 #[serde(default)]
216 pub base: Box<CardTraitBase>,
217 pub modes: Vec<StaticMode>,
218 pub ignore_effect_cards: Vec<CardId>,
219 pub ignore_effect_players: Vec<PlayerId>,
220 pub may_play_turn: i32,
221 #[serde(default)]
226 pub svars: HashMap<String, String>,
227 #[serde(skip)]
228 pub ir: StaticAbilityIr,
229}
230
231#[derive(Debug, Clone, Default)]
232pub struct StaticAbilityIr {
233 pub card_trait_requirements: CardTraitRequirementsIr,
234 pub valid_card: Option<CompiledSelector>,
235 pub valid_card_text: Option<String>,
236 pub valid_cards_text: Option<String>,
237 pub valid_player: Option<CompiledSelector>,
238 pub affected: Option<CompiledSelector>,
239 pub affected_text: Option<String>,
240 pub affected_zone: Option<ZoneType>,
241 pub affected_zone_text: Option<String>,
242 pub affected_zones: Vec<ZoneType>,
243 pub valid_zone: Vec<ZoneType>,
244 pub may_play: bool,
245 pub may_play_alt_mana_cost: Option<String>,
249 pub counter_type_text: Option<String>,
250 pub counter_type: Option<CounterType>,
251 pub caster: Option<CompiledSelector>,
252 pub activator: Option<CompiledSelector>,
253 pub player: Option<CompiledSelector>,
254 pub valid_sa: Option<String>,
255 pub valid_mode: Option<String>,
256 pub valid_trigger: Option<String>,
257 pub valid_cause: Option<CompiledSelector>,
258 pub valid_cause_text: Option<String>,
259 pub valid_source: Option<CompiledSelector>,
260 pub valid_activator: Option<CompiledSelector>,
261 pub for_cost: Option<bool>,
262 pub is_combat: Option<bool>,
263 pub origin_zones: Vec<ZoneType>,
264 pub destination_zones: Vec<ZoneType>,
265 pub combat_damage: Option<bool>,
266 pub sorcery_speed: bool,
267 pub only_sorcery_speed: bool,
268 pub cmc_gt: Option<String>,
269 pub num_limit_each_turn: Option<i32>,
270 pub activator_raw: Option<String>,
271 pub cost: Option<String>,
272 pub gain_control_text: Option<String>,
273 pub add_power_text: Option<String>,
274 pub add_toughness_text: Option<String>,
275 pub add_type_text: Option<String>,
276 pub set_power_text: Option<String>,
277 pub set_toughness_text: Option<String>,
278 pub add_keyword_text: Option<String>,
279 pub remove_all_abilities: bool,
280 pub add_ability_text: Option<String>,
281 pub add_trigger_text: Option<String>,
282 pub add_static_ability_text: Option<String>,
283 pub adjust_land_plays_text: Option<String>,
284 pub type_filter: Option<String>,
285 pub mana_conversion: Option<String>,
286 pub except_cause_text: Option<String>,
287 pub restriction_text: Option<String>,
288 pub x_alternative_text: Option<String>,
289 pub announce_text: Option<String>,
290 pub mana_restriction_text: Option<String>,
291 pub stack_description_text: Option<String>,
292 pub cost_desc_text: Option<String>,
293 pub description_text: Option<String>,
294 pub draw_limit: Option<i32>,
295 pub set_max_hand_size: Option<String>,
300 pub raise_max_hand_size: Option<String>,
303 pub named_text: Option<String>,
304 pub trigger_text: Option<String>,
305 pub valid_defender: Option<CompiledSelector>,
306 pub valid_defender_text: Option<String>,
307 pub max_attackers: Option<String>,
308 pub max_blockers: Option<String>,
309 pub valid_attacked: Option<CompiledSelector>,
310 pub valid_creature: Option<CompiledSelector>,
311 pub valid_entity: Option<CompiledSelector>,
312 pub is_present: Option<CompiledSelector>,
313 pub valid_attacker: Option<CompiledSelector>,
314 pub valid_blocker: Option<CompiledSelector>,
315 pub valid_blocker_text: Option<String>,
316 pub valid_attacker_relative: Option<CompiledSelector>,
317 pub has_valid_attacker_relative: bool,
318 pub valid_blocker_relative: Option<CompiledSelector>,
319 pub has_valid_blocker_relative: bool,
320 pub unless_defender_text: Option<String>,
321 pub kw_text: Option<String>,
322 pub valid_keyword_text: Option<String>,
323 pub value_text: Option<String>,
324 pub mana_type_text: Option<String>,
325 pub result_text: Option<String>,
326 pub new_time_text: Option<String>,
327 pub present_compare_text: Option<String>,
328 pub check_svar_text: Option<String>,
329 pub svar_compare_text: Option<String>,
330 pub min_text: Option<String>,
331 pub max_text: Option<String>,
332 pub additional_text: Option<String>,
333 pub attacker_text: Option<String>,
334 pub target_text: Option<String>,
335 pub trigger: bool,
336 pub twice: bool,
337 pub only_source_abs: bool,
338 pub optional: bool,
339 pub num_value: Option<i32>,
340 pub defender_not_nearest_to_you_in_chosen_direction: bool,
341 pub effect_zone_all: bool,
342 pub effect_zones: Vec<ZoneType>,
343 pub valid_target: Option<CompiledSelector>,
344 pub valid_target_text: Option<String>,
345 pub valid_spell: Option<String>,
346 pub for_each_shard: Option<String>,
347 pub amount: Option<String>,
348 pub min_mana: Option<i32>,
349 pub raise_to: bool,
350 pub may_play_ignore_type: bool,
351 pub may_play_ignore_color: bool,
352 pub may_play_snow_ignore_color: bool,
353 pub color: Option<String>,
354 pub ignore_generic: bool,
355 pub only_first_spell: bool,
356 pub unless_valid_target: bool,
357 pub relative: bool,
358 pub target: Option<CompiledSelector>,
359 pub valid_card_to_target: Option<CompiledSelector>,
360 pub exception_sba: bool,
361 pub exceptions: Option<CompiledSelector>,
362 pub has_valid_card: bool,
363 pub has_valid_player: bool,
364 pub max_num: Option<i32>,
365 pub active_zones: Vec<ZoneType>,
366 pub phases_text: Option<String>,
367 pub condition_text: Option<String>,
368 pub player_turn_text: Option<String>,
369 pub top_card_of_library_is: Option<String>,
370 pub class_level_min: Option<i32>,
371 pub check_third_svar: Option<String>,
372 pub third_svar_compare: Option<String>,
373 pub check_fourth_svar: Option<String>,
374 pub fourth_svar_compare: Option<String>,
375 pub add_power: bool,
376 pub add_toughness: bool,
377 pub set_power: bool,
378 pub set_toughness: bool,
379 pub add_keyword: bool,
380 pub gain_control_param: bool,
381 pub add_type: bool,
382 pub remove_type: bool,
383 pub add_color: bool,
384 pub remove_color: bool,
385 pub set_color: bool,
386 pub characteristic_defining: bool,
387 pub has_text_layer_key: bool,
388 pub has_type_layer_key: bool,
389 pub has_color_layer_key: bool,
390 pub has_ability_layer_key: bool,
391 pub has_rules_layer_key: bool,
392 pub has_zone_keys: bool,
393 pub has_condition_keys: bool,
394}
395
396impl StaticAbilityIr {
397 fn from_params(params: &Params) -> Self {
398 let raw = params.inner();
399 Self {
400 card_trait_requirements: CardTraitRequirementsIr::from_key_values(
401 params.iter(),
402 params.selector_untracked(keys::IS_PRESENT).cloned(),
403 params.selector_untracked("IsPresent2").cloned(),
404 ),
405 valid_card: params.selector_untracked(keys::VALID_CARD).cloned(),
406 valid_card_text: raw.get(keys::VALID_CARD).map(String::to_string),
407 valid_cards_text: raw.get(keys::VALID_CARDS).map(String::to_string),
408 valid_player: params.selector_untracked(keys::VALID_PLAYER).cloned(),
409 affected: params.selector_untracked(keys::AFFECTED).cloned(),
410 affected_text: raw.get(keys::AFFECTED).map(String::to_string),
411 affected_zone: raw
412 .get(keys::AFFECTED_ZONE)
413 .map(String::as_str)
414 .and_then(ZoneType::from_str_compat),
415 affected_zone_text: raw.get(keys::AFFECTED_ZONE).map(String::to_string),
416 affected_zones: zone_list(raw.get(keys::AFFECTED_ZONE).map(String::as_str)),
417 valid_zone: zone_list(raw.get(keys::VALID_ZONE).map(String::as_str)),
418 may_play: raw
419 .get(keys::MAY_PLAY)
420 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
421 may_play_alt_mana_cost: raw.get(keys::MAY_PLAY_ALT_MANA_COST).map(String::to_string),
422 counter_type_text: raw.get(keys::COUNTER_TYPE).map(String::to_string),
423 counter_type: raw
424 .get(keys::COUNTER_TYPE)
425 .map(|value| crate::card::counter_type::parse_counter_type(value)),
426 caster: params.selector_untracked(keys::CASTER).cloned(),
427 activator: params.selector_untracked(keys::ACTIVATOR).cloned(),
428 player: params.selector_untracked(keys::PLAYER).cloned(),
429 valid_sa: raw.get(keys::VALID_SA).map(String::to_string),
430 valid_mode: raw.get(keys::VALID_MODE).map(String::to_string),
431 valid_trigger: raw.get(keys::VALID_TRIGGER).map(String::to_string),
432 valid_cause: params.selector_untracked(keys::VALID_CAUSE).cloned(),
433 valid_cause_text: raw.get(keys::VALID_CAUSE).map(String::to_string),
434 valid_source: params.selector_untracked(keys::VALID_SOURCE).cloned(),
435 valid_activator: params.selector_untracked(keys::VALID_ACTIVATOR).cloned(),
436 for_cost: raw
437 .get(keys::FOR_COST)
438 .map(|value| value.eq_ignore_ascii_case("True")),
439 is_combat: raw
440 .get(keys::IS_COMBAT)
441 .map(|value| value.eq_ignore_ascii_case("True")),
442 origin_zones: zone_list(raw.get(keys::ORIGIN).map(String::as_str)),
443 destination_zones: zone_list(raw.get(keys::DESTINATION).map(String::as_str)),
444 combat_damage: raw
445 .get(keys::COMBAT_DAMAGE)
446 .map(|value| value.eq_ignore_ascii_case("True")),
447 sorcery_speed: raw.get(keys::SORCERY_SPEED).is_some(),
448 only_sorcery_speed: raw.get("OnlySorcerySpeed").is_some(),
449 cmc_gt: raw.get("cmcGT").map(String::to_string),
450 num_limit_each_turn: raw
451 .get("NumLimitEachTurn")
452 .and_then(|value| value.parse().ok()),
453 activator_raw: raw.get(keys::ACTIVATOR).map(String::to_string),
454 cost: raw.get(keys::COST).map(String::to_string),
455 gain_control_text: raw.get(keys::GAIN_CONTROL).map(String::to_string),
456 add_power_text: raw.get(keys::ADD_POWER).map(String::to_string),
457 add_toughness_text: raw.get(keys::ADD_TOUGHNESS).map(String::to_string),
458 add_type_text: raw.get(keys::ADD_TYPE).map(String::to_string),
459 set_power_text: raw.get(keys::SET_POWER).map(String::to_string),
460 set_toughness_text: raw.get(keys::SET_TOUGHNESS).map(String::to_string),
461 add_keyword_text: raw.get(keys::ADD_KEYWORD).map(String::to_string),
462 remove_all_abilities: raw
463 .get(keys::REMOVE_ALL_ABILITIES)
464 .is_some_and(|value| value.eq_ignore_ascii_case("true")),
465 add_ability_text: raw.get(keys::ADD_ABILITY).map(String::to_string),
466 add_trigger_text: raw.get(keys::ADD_TRIGGER).map(String::to_string),
467 add_static_ability_text: raw.get("AddStaticAbility").map(String::to_string),
468 adjust_land_plays_text: raw.get(keys::ADJUST_LAND_PLAYS).map(String::to_string),
469 type_filter: raw.get(keys::TYPE).map(String::to_string),
470 mana_conversion: raw.get(keys::MANA_CONVERSION).map(String::to_string),
471 except_cause_text: raw.get(keys::EXCEPT_CAUSE).map(String::to_string),
472 restriction_text: raw.get(keys::RESTRICTION).map(String::to_string),
473 x_alternative_text: raw.get("XAlternative").map(String::to_string),
474 announce_text: raw.get("Announce").map(String::to_string),
475 mana_restriction_text: raw.get("ManaRestriction").map(String::to_string),
476 stack_description_text: raw.get("StackDescription").map(String::to_string),
477 cost_desc_text: raw.get("CostDesc").map(String::to_string),
478 description_text: raw.get(keys::DESCRIPTION).map(String::to_string),
479 draw_limit: raw
480 .get(keys::DRAW_LIMIT)
481 .and_then(|value| value.parse().ok()),
482 set_max_hand_size: raw.get("SetMaxHandSize").map(String::to_string),
483 raise_max_hand_size: raw.get("RaiseMaxHandSize").map(String::to_string),
484 named_text: raw.get("Named").map(String::to_string),
485 trigger_text: raw.get(keys::TRIGGER).map(String::to_string),
486 valid_defender: params.selector_untracked(keys::VALID_DEFENDER).cloned(),
487 valid_defender_text: raw.get(keys::VALID_DEFENDER).map(String::to_string),
488 max_attackers: raw.get(keys::MAX_ATTACKERS).map(String::to_string),
489 max_blockers: raw.get(keys::MAX_BLOCKERS).map(String::to_string),
490 valid_attacked: params.selector_untracked(keys::VALID_ATTACKED).cloned(),
491 valid_creature: params.selector_untracked(keys::VALID_CREATURE).cloned(),
492 valid_entity: params.selector_untracked(keys::VALID_ENTITY).cloned(),
493 is_present: params.selector_untracked(keys::IS_PRESENT).cloned(),
494 valid_attacker: params.selector_untracked(keys::VALID_ATTACKER).cloned(),
495 valid_blocker: params.selector_untracked(keys::VALID_BLOCKER).cloned(),
496 valid_blocker_text: raw.get(keys::VALID_BLOCKER).map(String::to_string),
497 valid_attacker_relative: params
498 .selector_untracked(keys::VALID_ATTACKER_RELATIVE)
499 .cloned(),
500 has_valid_attacker_relative: raw.contains_key(keys::VALID_ATTACKER_RELATIVE),
501 valid_blocker_relative: params
502 .selector_untracked(keys::VALID_BLOCKER_RELATIVE)
503 .cloned(),
504 has_valid_blocker_relative: raw.contains_key(keys::VALID_BLOCKER_RELATIVE),
505 unless_defender_text: raw.get(keys::UNLESS_DEFENDER).map(String::to_string),
506 kw_text: raw.get(keys::KW).map(String::to_string),
507 valid_keyword_text: raw.get(keys::VALID_KEYWORD).map(String::to_string),
508 value_text: raw.get(keys::VALUE).map(String::to_string),
509 mana_type_text: raw.get(keys::MANA_TYPE).map(String::to_string),
510 result_text: raw.get(keys::RESULT).map(String::to_string),
511 new_time_text: raw.get(keys::NEW_TIME).map(String::to_string),
512 present_compare_text: raw.get(keys::PRESENT_COMPARE).map(String::to_string),
513 check_svar_text: raw.get(keys::CHECK_SVAR).map(String::to_string),
514 svar_compare_text: raw.get(keys::SVAR_COMPARE).map(String::to_string),
515 min_text: raw.get(keys::MIN).map(String::to_string),
516 max_text: raw.get(keys::MAX).map(String::to_string),
517 additional_text: raw.get(keys::ADDITIONAL).map(String::to_string),
518 attacker_text: raw.get(keys::ATTACKER).map(String::to_string),
519 target_text: raw.get(keys::TARGET).map(String::to_string),
520 trigger: raw.contains_key(keys::TRIGGER),
521 twice: raw.contains_key(keys::TWICE),
522 only_source_abs: raw.contains_key(keys::ONLY_SOURCE_ABS),
523 optional: raw.contains_key(keys::OPTIONAL),
524 num_value: raw.get(keys::NUM).and_then(|value| value.parse().ok()),
525 defender_not_nearest_to_you_in_chosen_direction: raw
526 .contains_key(keys::DEFENDER_NOT_NEAREST_TO_YOU_IN_CHOSEN_DIRECTION),
527 effect_zone_all: raw
528 .get(keys::EFFECT_ZONE)
529 .or_else(|| raw.get(keys::AFFECTED_ZONE))
530 .is_some_and(|value| value.eq_ignore_ascii_case("All")),
531 effect_zones: zone_list(
532 raw.get(keys::EFFECT_ZONE)
533 .or_else(|| raw.get(keys::AFFECTED_ZONE))
534 .map(String::as_str),
535 ),
536 valid_target: params.selector_untracked(keys::VALID_TARGET).cloned(),
537 valid_target_text: raw.get(keys::VALID_TARGET).map(String::to_string),
538 valid_spell: raw.get(keys::VALID_SPELL).map(String::to_string),
539 for_each_shard: raw.get(keys::FOR_EACH_SHARD).map(String::to_string),
540 amount: raw.get(keys::AMOUNT).map(String::to_string),
541 min_mana: raw.get(keys::MIN_MANA).and_then(|value| value.parse().ok()),
542 raise_to: raw
543 .get("RaiseTo")
544 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
545 may_play_ignore_type: raw.contains_key("MayPlayIgnoreType"),
546 may_play_ignore_color: raw.contains_key("MayPlayIgnoreColor"),
547 may_play_snow_ignore_color: raw.contains_key("MayPlaySnowIgnoreColor"),
548 color: raw.get(keys::COLOR).map(String::to_string),
549 ignore_generic: raw
550 .get("IgnoreGeneric")
551 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
552 only_first_spell: raw
553 .get("OnlyFirstSpell")
554 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
555 unless_valid_target: raw
556 .get("UnlessValidTarget")
557 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
558 relative: raw
559 .get("Relative")
560 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
561 target: params.selector_untracked(keys::TARGET).cloned(),
562 valid_card_to_target: params
563 .selector_untracked(keys::VALID_CARD_TO_TARGET)
564 .cloned(),
565 exception_sba: raw.get(keys::EXCEPTION_SBA).is_some(),
566 exceptions: params.selector_untracked(keys::EXCEPTIONS).cloned(),
567 has_valid_card: raw.contains_key(keys::VALID_CARD),
568 has_valid_player: raw.contains_key(keys::VALID_PLAYER),
569 max_num: raw.get(keys::MAX_NUM).and_then(|value| value.parse().ok()),
570 active_zones: zone_list(raw.get(keys::ACTIVE_ZONES).map(String::as_str)),
571 phases_text: raw.get(keys::PHASES).map(String::to_string),
572 condition_text: raw.get(keys::CONDITION).map(String::to_string),
573 player_turn_text: raw.get(keys::PLAYER_TURN).map(String::to_string),
574 top_card_of_library_is: raw.get("TopCardOfLibraryIs").map(String::to_string),
575 class_level_min: raw.get("ClassLevel").and_then(|value| value.parse().ok()),
576 check_third_svar: raw.get("CheckThirdSVar").map(String::to_string),
577 third_svar_compare: raw.get("ThirdSVarCompare").map(String::to_string),
578 check_fourth_svar: raw.get("CheckFourthSVar").map(String::to_string),
579 fourth_svar_compare: raw.get("FourthSVarCompare").map(String::to_string),
580 add_power: raw.contains_key(keys::ADD_POWER),
581 add_toughness: raw.contains_key(keys::ADD_TOUGHNESS),
582 set_power: raw.contains_key(keys::SET_POWER),
583 set_toughness: raw.contains_key(keys::SET_TOUGHNESS),
584 add_keyword: raw.contains_key(keys::ADD_KEYWORD),
585 gain_control_param: raw.contains_key(keys::GAIN_CONTROL),
586 add_type: raw.contains_key(keys::ADD_TYPE),
587 remove_type: raw.contains_key(keys::REMOVE_TYPE),
588 add_color: raw.contains_key(keys::ADD_COLOR),
589 remove_color: raw.contains_key(keys::REMOVE_COLOR),
590 set_color: raw.contains_key(keys::SET_COLOR),
591 characteristic_defining: raw
592 .get(keys::CHARACTERISTIC_DEFINING)
593 .is_some_and(|v| v.eq_ignore_ascii_case("True")),
594 has_text_layer_key: params.contains_any_key(&[
595 keys::CHANGE_COLOR_WORDS_TO,
596 keys::GAIN_TEXT_OF,
597 keys::ADD_NAMES,
598 keys::SET_NAME,
599 keys::INCORPORATE,
600 keys::MANA_COST,
601 ]),
602 has_type_layer_key: params.contains_any_key(&[
603 keys::ADD_TYPE,
604 keys::REMOVE_TYPE,
605 keys::ADD_ALL_CREATURE_TYPES,
606 keys::REMOVE_CARD_TYPES,
607 keys::REMOVE_SUB_TYPES,
608 keys::REMOVE_SUPER_TYPES,
609 keys::REMOVE_LAND_TYPES,
610 keys::REMOVE_CREATURE_TYPES,
611 keys::REMOVE_ARTIFACT_TYPES,
612 keys::REMOVE_ENCHANTMENT_TYPES,
613 ]),
614 has_color_layer_key: params.contains_any_key(&[
615 keys::ADD_COLOR,
616 keys::REMOVE_COLOR,
617 keys::SET_COLOR,
618 ]),
619 has_ability_layer_key: params.contains_any_key(&[
620 keys::REMOVE_ALL_ABILITIES,
621 keys::REMOVE_NON_MANA_ABILITIES,
622 keys::GAINS_ABILITIES_OF,
623 keys::GAINS_ABILITIES_OF_DEFINED,
624 keys::GAINS_TRIGGER_ABS_OF,
625 keys::ADD_KEYWORD,
626 keys::ADD_ABILITY,
627 keys::ADD_TRIGGER,
628 keys::ADD_REPLACEMENT_EFFECT,
629 keys::ADD_STATIC_ABILITY,
630 keys::ADD_SVAR,
631 keys::CANT_HAVE_KEYWORD,
632 keys::SHARE_REMEMBERED_KEYWORDS,
633 keys::REMOVE_KEYWORD,
634 ]),
635 has_rules_layer_key: params.contains_any_key(&[
636 keys::ADD_HIDDEN_KEYWORD,
637 keys::MAY_PLAY,
638 keys::IGNORE_EFFECT_COST,
639 keys::GOAD,
640 keys::CAN_BLOCK_ANY,
641 keys::CAN_BLOCK_AMOUNT,
642 keys::ADJUST_LAND_PLAYS,
643 keys::CONTROL_VOTE,
644 keys::ADDITIONAL_VOTE,
645 keys::ADDITIONAL_OPTIONAL_VOTE,
646 keys::DECLARES_ATTACKERS,
647 keys::DECLARES_BLOCKERS,
648 ]),
649 has_zone_keys: params.contains_any_key(STATIC_ZONE_KEYS),
650 has_condition_keys: params.contains_any_key(STATIC_CONDITION_KEYS),
651 }
652 }
653}
654
655fn zone_list(raw: Option<&str>) -> Vec<ZoneType> {
656 raw.map(|zones| {
657 zones
658 .split(',')
659 .filter_map(|zone| ZoneType::from_str_compat(zone.trim()))
660 .collect()
661 })
662 .unwrap_or_default()
663}
664
665impl StaticAbility {
666 fn sync_trait_base_params(&mut self) {
667 self.base.set_svars(self.svars.clone());
668 }
669
670 pub fn check_mode(&self, mode: &StaticMode) -> bool {
671 match mode {
672 StaticMode::Other(query) => self
673 .modes
674 .iter()
675 .any(|m| matches!(m, StaticMode::Other(s) if s.eq_ignore_ascii_case(query))),
676 other => self.modes.iter().any(|m| m == other),
677 }
678 }
679
680 pub fn unknown_modes(&self) -> impl Iterator<Item = &str> {
681 self.modes.iter().filter_map(|mode| match mode {
682 StaticMode::Other(value) => Some(value.as_str()),
683 _ => None,
684 })
685 }
686
687 pub fn parsed_but_unimplemented_modes(&self) -> impl Iterator<Item = &StaticMode> {
691 self.modes.iter().filter(|mode| {
692 matches!(
693 mode,
694 StaticMode::OptionalCost
695 | StaticMode::AttackRequirement
696 | StaticMode::PlayerMustAttack
697 )
698 })
699 }
700
701 pub fn zones_check(&self, source_zone: ZoneType) -> bool {
702 if !self.ir.has_zone_keys {
703 return source_zone == ZoneType::Battlefield;
704 }
705
706 let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
707 crate::perf::ParamsLookupScope::StaticAbility,
708 );
709 if !self.ir.active_zones.is_empty() {
710 return self.ir.active_zones.contains(&source_zone);
711 }
712 if self.ir.effect_zone_all {
713 return true;
714 }
715 if !self.ir.effect_zones.is_empty() {
716 return self.ir.effect_zones.contains(&source_zone);
717 }
718 source_zone == ZoneType::Battlefield
719 }
720
721 pub fn check_conditions(&self, source: &Card, game: &GameState) -> bool {
722 let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
723 crate::perf::ParamsLookupScope::StaticAbility,
724 );
725 if !self.zones_check(source.zone) {
726 return false;
727 }
728 if source.phased_out {
729 return false;
730 }
731 if !self.meets_card_trait_requirements(game, source, self) {
732 return false;
733 }
734
735 if !self.ir.has_condition_keys {
736 return true;
737 }
738
739 if let Some(phases) = self.ir.phases_text.as_deref() {
740 let current = format!("{:?}", game.turn.phase);
741 if !phases
742 .split(',')
743 .map(str::trim)
744 .any(|p| p.eq_ignore_ascii_case(¤t))
745 {
746 return false;
747 }
748 }
749
750 if let Some(condition) = self.ir.condition_text.as_deref() {
751 if condition.eq_ignore_ascii_case("MaxSpeed")
752 && game.player(source.controller).speed != 4
753 {
754 return false;
755 }
756 }
757
758 if let Some(player_turn) = self.ir.player_turn_text.as_deref() {
759 let active = game.turn.active_player;
760 let defined = crate::ability::effects::helpers::resolve_defined_players(
761 player_turn,
762 source.controller,
763 game,
764 );
765 let ok = defined.contains(&active);
766 if !ok {
767 return false;
768 }
769 }
770
771 if let Some(valid_top) = self.ir.top_card_of_library_is.as_deref() {
772 let top = game
773 .zone(ZoneType::Library, source.controller)
774 .peek_top()
775 .map(|cid| game.card(cid));
776 let Some(top_card) = top else {
777 return false;
778 };
779 if !crate::card::valid_filter::matches_valid_card_opt(Some(valid_top), top_card, source)
780 {
781 return false;
782 }
783 }
784
785 if let Some(min) = self.ir.class_level_min {
786 if source.class_level < min {
787 return false;
788 }
789 }
790
791 if let Some(check_name) = self.ir.check_third_svar.as_deref() {
792 let compare = self.ir.third_svar_compare.as_deref().unwrap_or("GE1");
793 if !crate::card::valid_filter::check_svar_requirement(
794 game, source, source, check_name, compare,
795 ) {
796 return false;
797 }
798 }
799 if let Some(check_name) = self.ir.check_fourth_svar.as_deref() {
800 let compare = self.ir.fourth_svar_compare.as_deref().unwrap_or("GE1");
801 if !crate::card::valid_filter::check_svar_requirement(
802 game, source, source, check_name, compare,
803 ) {
804 return false;
805 }
806 }
807
808 true
809 }
810
811 pub fn check_conditions_full(
812 &self,
813 mode: &StaticMode,
814 source: &Card,
815 game: &GameState,
816 ) -> bool {
817 self.check_mode(mode) && self.check_conditions(source, game)
818 }
819
820 pub fn is_active_for(&self, mode: StaticMode, source_zone: ZoneType) -> bool {
821 self.check_mode(&mode) && self.zones_check(source_zone)
822 }
823
824 pub fn add_ignore_effect_players(&mut self, player: PlayerId) {
825 if !self.ignore_effect_players.contains(&player) {
826 self.ignore_effect_players.push(player);
827 }
828 }
829
830 pub fn clear_ignore_effects(&mut self) {
831 self.ignore_effect_cards.clear();
832 self.ignore_effect_players.clear();
833 }
834
835 pub fn inc_may_play_turn(&mut self) {
836 self.may_play_turn += 1;
837 }
838
839 pub fn reset_may_play_turn(&mut self) {
840 self.may_play_turn = 0;
841 }
842
843 pub fn copy(&self) -> Self {
844 self.clone()
845 }
846}
847
848#[derive(Debug, Clone, Default, Serialize, Deserialize)]
864pub struct CardFilter {
865 pub creatures_only: bool,
867 pub controller_only: bool,
869 pub owner_only: bool,
871 pub other_only: bool,
873 pub commander_only: bool,
875 pub subtype: Option<String>,
877 pub nonland_only: bool,
879 pub land_only: bool,
881 pub required_color: Option<ColorSet>,
884 pub colorless_only: bool,
886 pub attacking_you: bool,
889 pub card_name: Option<String>,
891 pub token_only: bool,
893 pub shares_color_with_source: bool,
895 pub shares_color_with_equipped: bool,
897 pub shares_creature_type_with_source: bool,
899 pub shares_creature_type_with_equipped: bool,
901}
902
903impl CardFilter {
904 pub fn parse(s: &str) -> Self {
906 let mut f = CardFilter::default();
907 let mut parts = s.split('+');
910 let base = parts.next().unwrap_or("").trim();
912 for seg in base.split('.') {
913 Self::apply_segment(&mut f, seg.trim());
914 }
915 for part in parts {
917 Self::apply_segment(&mut f, part.trim());
918 }
919 f
920 }
921
922 fn apply_segment(f: &mut CardFilter, seg: &str) {
923 match seg {
924 "Creature" => f.creatures_only = true,
925 "Permanent" | "Card" | "" => {}
927 "nonLand" | "NonLand" => f.nonland_only = true,
928 "Land" => f.land_only = true,
929 "YouControl" | "YouCtrl" => f.controller_only = true,
930 "YouOwn" => f.owner_only = true,
931 "Other" => f.other_only = true,
932 "IsCommander" => f.commander_only = true,
933 "White" => f.required_color = Some(ColorSet::WHITE),
935 "Blue" => f.required_color = Some(ColorSet::BLUE),
936 "Black" => f.required_color = Some(ColorSet::BLACK),
937 "Red" => f.required_color = Some(ColorSet::RED),
938 "Green" => f.required_color = Some(ColorSet::GREEN),
939 "Colorless" => f.colorless_only = true,
940 "attackingYou" => f.attacking_you = true,
941 "token" | "Token" => f.token_only = true,
942 "SharesColorWith" => f.shares_color_with_source = true,
943 "sharesCreatureTypeWith" => f.shares_creature_type_with_source = true,
944 "SharesColorWith Equipped" => f.shares_color_with_equipped = true,
945 "sharesCreatureTypeWith Equipped" => f.shares_creature_type_with_equipped = true,
946 s if s.starts_with("named") => {
947 f.card_name = Some(s["named".len()..].to_string());
948 }
949 s => {
950 if f.subtype.is_none() {
952 f.subtype = Some(s.to_string());
953 }
954 }
955 }
956 }
957
958 pub fn matches(&self, card: &Card, source: &Card) -> bool {
961 if self.creatures_only && !card.is_creature() {
962 return false;
963 }
964 if self.controller_only && card.controller != source.controller {
965 return false;
966 }
967 if self.owner_only && card.owner != source.controller {
968 return false;
969 }
970 if self.other_only && card.id == source.id {
971 return false;
972 }
973 if self.commander_only && !card.is_commander {
974 return false;
975 }
976 if let Some(ref sub) = self.subtype {
977 if !card.has_subtype(sub) {
978 return false;
979 }
980 }
981 if self.nonland_only && card.is_land() {
982 return false;
983 }
984 if self.land_only && !card.is_land() {
985 return false;
986 }
987 if let Some(required) = self.required_color {
988 if !card.color.shares_color_with(required) {
989 return false;
990 }
991 }
992 if self.colorless_only && !card.color.is_colorless() {
993 return false;
994 }
995 if self.attacking_you && card.attacking_player != Some(source.controller) {
996 return false;
997 }
998 if let Some(ref name) = self.card_name {
999 if card.card_name != *name {
1000 return false;
1001 }
1002 }
1003 if self.token_only && !card.is_token {
1004 return false;
1005 }
1006 if self.shares_color_with_source {
1007 if card.color.is_colorless() || source.color.is_colorless() {
1008 return false;
1009 }
1010 if !card.color.shares_color_with(source.color) {
1011 return false;
1012 }
1013 }
1014 if self.shares_creature_type_with_source && !shares_creature_type_with(card, source) {
1015 return false;
1016 }
1017 true
1018 }
1019
1020 pub fn matches_with_game(&self, card: &Card, source: &Card, game: &GameState) -> bool {
1023 if !self.matches(card, source) {
1024 return false;
1025 }
1026 if self.shares_color_with_equipped {
1027 let Some(equipped_id) = source.attached_to else {
1028 return false;
1029 };
1030 let equipped = game.card(equipped_id);
1031 if card.color.is_colorless() || equipped.color.is_colorless() {
1032 return false;
1033 }
1034 if !card.color.shares_color_with(equipped.color) {
1035 return false;
1036 }
1037 }
1038 if self.shares_creature_type_with_equipped {
1039 let Some(equipped_id) = source.attached_to else {
1040 return false;
1041 };
1042 let equipped = game.card(equipped_id);
1043 if !shares_creature_type_with(card, equipped) {
1044 return false;
1045 }
1046 }
1047 true
1048 }
1049}
1050
1051fn shares_creature_type_with(a: &Card, b: &Card) -> bool {
1052 a.shares_creature_type_with(b)
1053}
1054
1055pub fn parse_static_ability(raw: &str) -> Option<StaticAbility> {
1072 let trimmed = raw.trim();
1073 let body = if let Some(rest) = trimmed.strip_prefix("S$ ") {
1076 rest
1077 } else if let Some(rest) = trimmed.strip_prefix("S:") {
1078 rest.trim_start()
1079 } else if trimmed.starts_with("Mode$ ") {
1080 trimmed
1081 } else {
1082 return None;
1083 };
1084
1085 let params = Params::from_raw(body);
1087
1088 let modes = match params.get(keys::MODE) {
1089 Some(value) => static_ability_mode::set_value_of(value),
1090 None => return None,
1091 };
1092 if modes.is_empty() {
1093 return None;
1094 }
1095
1096 let ir = StaticAbilityIr::from_params(¶ms);
1097 let mut st_ab = StaticAbility {
1098 base: Box::new(CardTraitBase::default()),
1099 modes,
1100 ignore_effect_cards: Vec::new(),
1101 ignore_effect_players: Vec::new(),
1102 may_play_turn: 0,
1103 svars: HashMap::new(),
1104 ir,
1105 };
1106 st_ab.sync_trait_base_params();
1107 Some(st_ab)
1108}
1109
1110impl HasSVars for StaticAbility {
1111 fn get_svar(&self, name: &str) -> Option<&str> {
1112 self.svars.get(name).map(String::as_str)
1113 }
1114
1115 fn set_svar(&mut self, name: String, value: String) {
1116 self.svars.insert(name.clone(), value.clone());
1117 self.base.set_svar(name, value);
1118 }
1119
1120 fn set_svars(&mut self, new_svars: HashMap<String, String>) {
1121 self.svars = new_svars.clone();
1122 self.base.set_svars(new_svars);
1123 }
1124
1125 fn get_svars(&self) -> &HashMap<String, String> {
1126 &self.svars
1127 }
1128
1129 fn remove_svar(&mut self, var: &str) {
1130 self.svars.remove(var);
1131 self.base.remove_svar(var);
1132 }
1133}
1134
1135impl CardTrait for StaticAbility {
1136 fn base(&self) -> &CardTraitBase {
1137 &self.base
1138 }
1139}
1140
1141impl CardTraitIrOwner for StaticAbility {
1142 type Ir = StaticAbilityIr;
1143
1144 fn ir(&self) -> &Self::Ir {
1145 &self.ir
1146 }
1147
1148 fn card_trait_requirements(&self) -> &CardTraitRequirementsIr {
1149 &self.ir.card_trait_requirements
1150 }
1151}
1152
1153#[cfg(test)]
1156mod tests {
1157 use super::*;
1158 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1159
1160 use crate::card::Card;
1161 use crate::ids::{CardId, PlayerId};
1162
1163 fn make_creature(id: u32, owner: u32, subtypes: &[&str]) -> Card {
1164 let type_str = if subtypes.is_empty() {
1165 "Creature".to_string()
1166 } else {
1167 format!("Creature - {}", subtypes.join(" "))
1168 };
1169 Card::new(
1170 CardId(id),
1171 "Test".to_string(),
1172 PlayerId(owner),
1173 CardTypeLine::parse(&type_str),
1174 ManaCost::parse("1 G"),
1175 ColorSet::GREEN,
1176 Some(2),
1177 Some(2),
1178 vec![],
1179 vec![],
1180 )
1181 }
1182
1183 fn make_land(id: u32, owner: u32) -> Card {
1184 Card::new(
1185 CardId(id),
1186 "Forest".to_string(),
1187 PlayerId(owner),
1188 CardTypeLine::parse("Basic Land - Forest"),
1189 ManaCost::parse(""),
1190 ColorSet::GREEN,
1191 None,
1192 None,
1193 vec![],
1194 vec![],
1195 )
1196 }
1197
1198 #[test]
1201 fn parse_continuous_anthem() {
1202 let raw = "S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1 | Description$ Creatures you control get +1/+1.";
1203 let sa = parse_static_ability(raw).expect("should parse");
1204 assert_eq!(sa.modes, vec![StaticMode::Continuous]);
1205 assert_eq!(sa.ir.add_power_text.as_deref(), Some("1"));
1206 assert_eq!(sa.ir.add_toughness_text.as_deref(), Some("1"));
1207 assert_eq!(
1208 crate::staticability::layer::classify_static_layers(&sa),
1209 vec![Layer::ModifyPT]
1210 );
1211 }
1212
1213 #[test]
1214 fn parse_etb_tapped() {
1215 let raw = "S$ Mode$ ETBTapped | Description$ This permanent enters the battlefield tapped.";
1216 let sa = parse_static_ability(raw).expect("should parse");
1217 assert_eq!(sa.modes, vec![StaticMode::ETBTapped]);
1218 assert!(crate::staticability::layer::classify_static_layers(&sa).is_empty());
1219 }
1220
1221 #[test]
1222 fn parse_cant_attack() {
1223 let raw = "S$ Mode$ CantAttack | Affected$ Creature.YouControl | Description$ Creatures you control can't attack.";
1224 let sa = parse_static_ability(raw).expect("should parse");
1225 assert_eq!(sa.modes, vec![StaticMode::CantAttack]);
1226 }
1227
1228 #[test]
1229 fn parse_keyword_grant() {
1230 let raw = "S$ Mode$ Continuous | Affected$ Creature.YouControl | AddKeyword$ Flying | Description$ Creatures you control have flying.";
1231 let sa = parse_static_ability(raw).expect("should parse");
1232 assert_eq!(
1233 crate::staticability::layer::classify_static_layers(&sa),
1234 vec![Layer::Ability]
1235 );
1236 assert_eq!(sa.ir.add_keyword_text.as_deref(), Some("Flying"));
1237 }
1238
1239 #[test]
1240 fn parse_set_pt() {
1241 let raw =
1242 "S$ Mode$ Continuous | Affected$ Creature.YouControl | SetPower$ 0 | SetToughness$ 1";
1243 let sa = parse_static_ability(raw).expect("should parse");
1244 assert_eq!(
1245 crate::staticability::layer::classify_static_layers(&sa),
1246 vec![Layer::SetPT]
1247 );
1248 }
1249
1250 #[test]
1251 fn parse_s_colon_prefix() {
1252 let raw =
1253 "S: Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 2 | AddToughness$ 2";
1254 let sa = parse_static_ability(raw).expect("should parse S: prefix");
1255 assert_eq!(sa.modes, vec![StaticMode::Continuous]);
1256 }
1257
1258 #[test]
1259 fn parse_multi_mode_cant_attack_block() {
1260 let raw = "S$ Mode$ CantAttack,CantBlock | Affected$ Creature.YouControl";
1261 let sa = parse_static_ability(raw).expect("should parse");
1262 assert_eq!(
1263 sa.modes,
1264 vec![StaticMode::CantAttack, StaticMode::CantBlock]
1265 );
1266 assert!(sa.check_mode(&StaticMode::CantAttack));
1267 assert!(sa.check_mode(&StaticMode::CantBlock));
1268 assert!(!sa.check_mode(&StaticMode::CantBeActivated));
1269 }
1270
1271 #[test]
1272 fn parse_multi_mode_three_restrictions() {
1273 let raw = "S$ Mode$ CantAttack,CantBlock,CantBeActivated | Affected$ Creature.YouControl";
1274 let sa = parse_static_ability(raw).expect("should parse");
1275 assert!(sa.check_mode(&StaticMode::CantAttack));
1276 assert!(sa.check_mode(&StaticMode::CantBlock));
1277 assert!(sa.check_mode(&StaticMode::CantBeActivated));
1278 }
1279
1280 #[test]
1281 fn parse_continuous_with_restrictions() {
1282 let raw = "S$ Mode$ Continuous,CantPlayLand,CantBeCast | Affected$ Card.YouControl";
1283 let sa = parse_static_ability(raw).expect("should parse");
1284 assert!(sa.check_mode(&StaticMode::Continuous));
1285 assert!(sa.check_mode(&StaticMode::CantPlayLand));
1286 assert!(sa.check_mode(&StaticMode::CantBeCast));
1287 }
1288
1289 #[test]
1290 fn non_static_line_returns_none() {
1291 assert!(parse_static_ability("AB$ Mana | Cost$ T | Produced$ G").is_none());
1292 assert!(parse_static_ability("T$ Mode$ ChangesZone").is_none());
1293 assert!(parse_static_ability("").is_none());
1294 }
1295
1296 #[test]
1299 fn filter_creature_you_control() {
1300 let f = CardFilter::parse("Creature.YouControl");
1301 assert!(f.creatures_only);
1302 assert!(f.controller_only);
1303 assert!(!f.other_only);
1304 assert!(f.subtype.is_none());
1305 }
1306
1307 #[test]
1308 fn filter_creature_other_you_control() {
1309 let f = CardFilter::parse("Creature.Other+YouControl");
1310 assert!(f.creatures_only);
1311 assert!(f.controller_only);
1312 assert!(f.other_only);
1313 }
1314
1315 #[test]
1316 fn filter_goblin_subtype() {
1317 let f = CardFilter::parse("Creature.Goblin+YouControl");
1318 assert!(f.creatures_only);
1319 assert!(f.controller_only);
1320 assert_eq!(f.subtype, Some("Goblin".to_string()));
1321 }
1322
1323 #[test]
1324 fn filter_matches_creature() {
1325 let source = make_creature(0, 0, &[]);
1326 let target = make_creature(1, 0, &[]);
1327 let f = CardFilter::parse("Creature.YouControl");
1328 assert!(f.matches(&target, &source));
1329 }
1330
1331 #[test]
1332 fn filter_excludes_opponent_creatures() {
1333 let source = make_creature(0, 0, &[]);
1334 let mut opp = make_creature(1, 1, &[]); opp.controller = PlayerId(1);
1336 let f = CardFilter::parse("Creature.YouControl");
1337 assert!(!f.matches(&opp, &source));
1338 }
1339
1340 #[test]
1341 fn filter_excludes_self_with_other() {
1342 let source = make_creature(0, 0, &[]);
1343 let f = CardFilter::parse("Creature.Other+YouControl");
1344 assert!(!f.matches(&source, &source));
1345 }
1346
1347 #[test]
1348 fn filter_excludes_land_with_nonland() {
1349 let source = make_creature(0, 0, &[]);
1350 let land = make_land(1, 0);
1351 let f = CardFilter::parse("Permanent.nonLand+YouControl");
1352 assert!(!f.matches(&land, &source));
1353 }
1354
1355 #[test]
1356 fn filter_subtype_goblin() {
1357 let source = make_creature(0, 0, &[]);
1358 let goblin = make_creature(1, 0, &["Goblin"]);
1359 let bear = make_creature(2, 0, &["Bear"]);
1360 let f = CardFilter::parse("Creature.Goblin+YouControl");
1361 assert!(f.matches(&goblin, &source));
1362 assert!(!f.matches(&bear, &source));
1363 }
1364
1365 fn make_white_creature(id: u32, owner: u32) -> Card {
1368 Card::new(
1369 CardId(id),
1370 "White Knight".to_string(),
1371 PlayerId(owner),
1372 CardTypeLine::parse("Creature - Human Knight"),
1373 ManaCost::parse("W W"),
1374 ColorSet::WHITE,
1375 Some(2),
1376 Some(2),
1377 vec![],
1378 vec![],
1379 )
1380 }
1381
1382 #[test]
1383 fn filter_color_white_parses() {
1384 let f = CardFilter::parse("Creature.White+YouCtrl");
1385 assert!(f.creatures_only);
1386 assert!(f.controller_only);
1387 assert_eq!(f.required_color, Some(ColorSet::WHITE));
1388 assert!(
1389 f.subtype.is_none(),
1390 "White should not be treated as a subtype"
1391 );
1392 }
1393
1394 #[test]
1395 fn filter_honor_of_the_pure_matches_white_creature() {
1396 let source = make_white_creature(0, 0); let white_ally = make_white_creature(1, 0);
1399 let green_ally = make_creature(2, 0, &[]); let white_opponent = make_white_creature(3, 1); let mut white_opponent = white_opponent;
1402 white_opponent.controller = PlayerId(1);
1403
1404 let f = CardFilter::parse("Creature.White+YouCtrl");
1405 assert!(f.matches(&white_ally, &source), "white ally should match");
1406 assert!(
1407 !f.matches(&green_ally, &source),
1408 "green creature should not match"
1409 );
1410 assert!(
1411 !f.matches(&white_opponent, &source),
1412 "opponent's white creature should not match"
1413 );
1414 }
1415
1416 #[test]
1417 fn filter_color_white_does_not_match_colorless() {
1418 let source = make_white_creature(0, 0);
1419 let colorless = Card::new(
1420 CardId(1),
1421 "Darksteel Myr".to_string(),
1422 PlayerId(0),
1423 CardTypeLine::parse("Artifact Creature - Myr"),
1424 ManaCost::parse("3"),
1425 ColorSet::COLORLESS,
1426 Some(0),
1427 Some(1),
1428 vec![],
1429 vec![],
1430 );
1431 let f = CardFilter::parse("Creature.White+YouCtrl");
1432 assert!(
1433 !f.matches(&colorless, &source),
1434 "colorless artifact should not be white"
1435 );
1436 }
1437}