Skip to main content

manabrew_engine/ability/
ability_ir.rs

1use std::collections::BTreeMap;
2
3use crate::ability::ProducedMana;
4use crate::card::counter_type::parse_counter_type;
5use crate::card::valid_filter::CardTraitRequirementsIr;
6use crate::card::CounterType;
7use crate::parsing::amount::AmountExpr;
8use crate::parsing::{
9    keys, parse_semantic_param_value, split_param_list_value, CompiledSelector, Params,
10    ParsedParams, SemanticParamValue,
11};
12use crate::spellability::{AbilityDuration, ReplaceDyingCondition, SpellAbilityMode};
13use forge_foundation::ZoneType;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum EffectIr {
17    DamageAll(NumericAmountIr),
18    DealDamage(DealDamageIr),
19    Draw(NumericAmountIr),
20    GainLife(NumericAmountIr),
21    LifeSet(NumericAmountIr),
22    LoseLife(NumericAmountIr),
23    Mill(NumericAmountIr),
24    Poison(NumericAmountIr),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DayTimeValue {
29    Day,
30    Night,
31    Switch,
32}
33
34impl DayTimeValue {
35    pub fn parse(raw: &str) -> Option<Self> {
36        match raw {
37            "Day" => Some(Self::Day),
38            "Night" => Some(Self::Night),
39            "Switch" => Some(Self::Switch),
40            _ => None,
41        }
42    }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum DebuffAllSuffixKeywords {
47    Walk,
48}
49
50impl DebuffAllSuffixKeywords {
51    pub fn parse(raw: &str) -> Option<Self> {
52        match raw {
53            "walk" => Some(Self::Walk),
54            _ => None,
55        }
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Default)]
60pub struct DebuffIr {
61    pub num_present: bool,
62    pub keywords: Vec<String>,
63    pub all_suffix_keywords: Option<DebuffAllSuffixKeywords>,
64}
65
66impl DebuffIr {
67    pub fn from_parsed(params: &ParsedParams<'_>) -> Self {
68        Self {
69            num_present: params.has(keys::NUM),
70            keywords: split_param_list_value(params.get(keys::KEYWORDS), " & "),
71            all_suffix_keywords: params
72                .get(keys::ALL_SUFFIX_KEYWORDS)
73                .and_then(DebuffAllSuffixKeywords::parse),
74        }
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Default)]
79pub struct SpellAbilityIr {
80    pub card_trait_requirements: CardTraitRequirementsIr,
81    pub effect: Option<EffectIr>,
82    pub mode_text: Option<String>,
83    pub discard_mode: Option<crate::ability::effects::discard_effect::DiscardMode>,
84    pub produced_ir: Option<ProducedMana>,
85    pub reflect_property: Option<String>,
86    pub color_or_type: Option<String>,
87    pub restrict_valid: Option<String>,
88    pub valid_filter_text: Option<String>,
89    pub valid_filter_selector: Option<CompiledSelector>,
90    pub adds_keywords: Option<String>,
91    pub adds_keywords_valid: Option<String>,
92    pub adds_counters: Option<String>,
93    pub adds_counters_valid: Option<String>,
94    pub triggers_when_spent: Option<String>,
95    pub valid_cards_text: Option<String>,
96    pub valid_cards_selector: Option<CompiledSelector>,
97    pub valid_card_text: Option<String>,
98    pub valid_card_selector: Option<CompiledSelector>,
99    pub valid_players_text: Option<String>,
100    pub valid_players_selector: Option<CompiledSelector>,
101    pub valid_player_text: Option<String>,
102    pub valid_player_selector: Option<CompiledSelector>,
103    pub valid_tgts_text: Option<String>,
104    pub valid_tgts_selector: Option<CompiledSelector>,
105    pub valid_target_text: Option<String>,
106    pub valid_target_selector: Option<CompiledSelector>,
107    pub all_valid_text: Option<String>,
108    pub all_valid_selector: Option<CompiledSelector>,
109    pub defined_name_text: Option<String>,
110    pub set_chosen_number: Option<String>,
111    pub starting_with: Option<String>,
112    pub repeat: Option<String>,
113    pub branch_condition_svar: Option<String>,
114    pub branch_condition_svar_compare: Option<String>,
115    pub true_sub_ability: Option<String>,
116    pub false_sub_ability: Option<String>,
117    pub play_cost_text: Option<String>,
118    pub unless_cost: Option<String>,
119    pub amount: Option<String>,
120    pub counter_num_text: Option<String>,
121    pub max_repeat: Option<String>,
122    pub precost_desc: Option<String>,
123    pub token_amount: Option<String>,
124    pub sac_valid: Option<String>,
125    pub defined: Option<DefinedExpr>,
126    pub defined_player: Option<DefinedExpr>,
127    pub defined_text: Option<String>,
128    pub defined_player_text: Option<String>,
129    pub controller_text: Option<String>,
130    pub effect_owner: Option<DefinedExpr>,
131    pub effect_owner_text: Option<String>,
132    pub origin_text: Option<String>,
133    pub origin_zone: Option<ZoneType>,
134    pub origin_zones: Vec<ZoneType>,
135    pub destination_text: Option<String>,
136    pub destination_zone: Option<ZoneType>,
137    pub destination_zone_2: Option<ZoneType>,
138    pub zone1: Option<ZoneType>,
139    pub zone2: Option<ZoneType>,
140    pub change_type: Option<String>,
141    pub change_type_selector: Option<CompiledSelector>,
142    pub primary_text: Option<String>,
143    pub secondary_text: Option<String>,
144    pub secondary_type_text: Option<String>,
145    pub library_position: Option<String>,
146    pub library_position_2: Option<String>,
147    pub library_position_alternative: Option<String>,
148    pub shuffle_raw: Option<String>,
149    pub no_shuffle: bool,
150    pub mandatory: bool,
151    pub toggle: bool,
152    pub can_repeat_modes: bool,
153    pub entwine: bool,
154    pub skip_untap: bool,
155    pub tapped: bool,
156    pub shuffle: bool,
157    pub optional: bool,
158    pub gain_control: bool,
159    pub gain_control_text: Option<String>,
160    pub forget_changed: bool,
161    pub adds_no_counter: bool,
162    pub no_regen: bool,
163    pub damage_map: bool,
164    pub remember_damaged_creature: bool,
165    pub num_dmg_present: bool,
166    pub boon: bool,
167    pub unique: bool,
168    pub exile_on_moved: bool,
169    pub forget_on_phased_in: bool,
170    pub forget_counter: bool,
171    pub radiance: bool,
172    pub chooser: Option<String>,
173    pub gains: Option<String>,
174    pub choices: Option<String>,
175    pub choices_selector: Option<CompiledSelector>,
176    pub debuff: DebuffIr,
177    pub for_each_text: Option<String>,
178    pub exclude: Option<String>,
179    pub object_text: Option<String>,
180    pub source_text: Option<String>,
181    pub attached_to: Option<String>,
182    pub attached_to_player_text: Option<String>,
183    pub attach_after_text: Option<String>,
184    pub destination_alternative: Option<String>,
185    pub select_prompt: Option<String>,
186    pub sub_ability_name: Option<String>,
187    pub name_text: Option<String>,
188    pub names_text: Option<String>,
189    pub choose_from_list_text: Option<String>,
190    pub choose_from_defined_cards: bool,
191    pub stack_id: Option<u32>,
192    pub token_script: Option<String>,
193    pub token_owner: Option<String>,
194    pub token_name_text: Option<String>,
195    pub token_power: Option<i32>,
196    pub token_toughness: Option<i32>,
197    pub token_types_text: Option<String>,
198    pub token_colors_text: Option<String>,
199    pub token_keywords_text: Option<String>,
200    pub token_until_text: Option<String>,
201    pub keyword_text: Option<String>,
202    pub with_counters_type_text: Option<String>,
203    pub with_counters_type: Option<CounterType>,
204    pub with_counters_amount_text: Option<String>,
205    pub change_num: usize,
206    pub hidden: bool,
207    pub skip_reorder: bool,
208    pub secretly: bool,
209    pub random_chosen: bool,
210    pub forget_other_remembered: bool,
211    pub remember_changed: bool,
212    pub imprint: bool,
213    pub face_down: bool,
214    pub exile_face_down: bool,
215    pub transformed: bool,
216    pub at_random_text: Option<String>,
217    pub at_random: bool,
218    pub reveal: bool,
219    pub reveal_true: bool,
220    pub reveal_text: Option<String>,
221    pub remember_drawn: bool,
222    pub remember_removed_cards: bool,
223    pub remember_destroyed: bool,
224    pub remember_abandoned: bool,
225    pub may_shuffle: bool,
226    pub remember_altered: bool,
227    pub alter_attribute_activate: bool,
228    pub alter_attribute_attributes: Vec<String>,
229    pub remember_amass: bool,
230    pub remember_flag: bool,
231    pub remember_chosen: bool,
232    pub imprint_chosen: bool,
233    pub remember_clasher: bool,
234    pub remember_cloaked: bool,
235    pub remember_discovered: bool,
236    pub remember_drafted: bool,
237    pub remember_exchanged: bool,
238    pub remember_investigating_players: bool,
239    pub remember_made: bool,
240    pub always_remember: bool,
241    pub remember_milled: bool,
242    pub change_zone_table: bool,
243    pub at_eot: Option<String>,
244    pub execute: Option<String>,
245    pub replace_with_text: Option<String>,
246    pub damage_amount_text: Option<String>,
247    pub counter_type_text: Option<String>,
248    pub counter_type: Option<CounterType>,
249    pub simple_counter_type_choice_path: bool,
250    pub modular: bool,
251    pub adapt: bool,
252    pub monstrosity: bool,
253    pub bloodthirst: bool,
254    pub renown: bool,
255    pub abilities: Option<String>,
256    pub replacement_effects: Option<String>,
257    pub static_abilities: Option<String>,
258    pub triggers: Option<String>,
259    pub imprint_cards: Option<String>,
260    pub remember_spell: Option<String>,
261    pub remember_lki: Option<String>,
262    pub remember_targets: bool,
263    pub forget_other_targets: bool,
264    pub remember_cost_mana: bool,
265    pub forget_on_moved_text: Option<String>,
266    pub forget_on_moved_zone: Option<ZoneType>,
267    pub replace_dying_defined: Option<DefinedExpr>,
268    pub replace_dying_defined_text: Option<String>,
269    pub replace_dying_valid: Option<String>,
270    pub replace_dying_zone_text: Option<String>,
271    pub replace_dying_zone: Option<ZoneType>,
272    pub remember_objects: Option<String>,
273    /// `RememberObjects$ Remembered` — lowered from the `" & "`-separated list
274    /// (Java splits on `" & "`) so runtime sites read a bool instead of
275    /// re-splitting the raw string.
276    pub remember_objects_remembered: bool,
277    /// `RememberObjects$ RememberedController` (Arcane Denial, Sudden
278    /// Substitution).
279    pub remember_objects_remembered_controller: bool,
280    /// `RememberObjects$ RememberedLKI`.
281    pub remember_objects_remembered_lki: bool,
282    /// `RememberObjects$ TriggeredAttackerLKICopy` (Teferi's Veil).
283    pub remember_objects_triggered_attacker_lki_copy: bool,
284    pub remember_number: bool,
285    pub remember_svar_amount: Option<String>,
286    pub remember_exiled: bool,
287    pub delayed_trigger_defined_player: Option<DefinedExpr>,
288    pub delayed_trigger_defined_player_text: Option<String>,
289    pub this_turn: bool,
290    pub condition_hellbent: bool,
291    pub condition_threshold: bool,
292    pub condition_metalcraft: bool,
293    pub condition_delirium: bool,
294    pub condition_revolt: bool,
295    pub condition_desert: bool,
296    pub condition_blessing: bool,
297    pub condition_kicked: bool,
298    pub condition_optional_paid: bool,
299    pub condition_optional_not_paid: bool,
300    pub condition_opponent_turn: bool,
301    pub condition_player_turn: Option<String>,
302    pub condition_cards_in_hand: Option<String>,
303    pub condition_phases: Option<String>,
304    pub condition_life_compare: Option<String>,
305    pub condition: Option<String>,
306    pub condition_check_svar: Option<String>,
307    pub condition_svar_compare: Option<String>,
308    pub condition_present: Option<String>,
309    pub condition_defined: Option<DefinedExpr>,
310    pub condition_defined_text: Option<String>,
311    pub condition_compare: Option<String>,
312    pub condition_zone_text: Option<String>,
313    pub condition_zone: Option<ZoneType>,
314    pub optional_present: bool,
315    pub remove_from_combat: bool,
316    pub num_att: Option<String>,
317    pub num_def: Option<String>,
318    pub types_text: Option<String>,
319    pub add_types: Option<String>,
320    pub set_color: Option<String>,
321    pub set_power: Option<String>,
322    pub set_toughness: Option<String>,
323    pub set_mana_cost: Option<String>,
324    pub clone_target: Option<String>,
325    pub kw: Option<String>,
326    pub pump_keywords: Option<String>,
327    pub add_keywords: Option<String>,
328    pub kw_choice: Option<String>,
329    pub up_to: bool,
330    /// `Upto$` for `DrawEffect`: decider chooses how many to draw (0..=NumCards).
331    /// Mirrors Java `DrawEffect.resolve`'s `final boolean upto = sa.hasParam("Upto")`.
332    pub upto: bool,
333    /// `OptionalDecider$` — optional-effect gatekeeper for the drawing player
334    /// (and similar effects). Stored as the raw player reference (e.g. "You").
335    pub optional_decider: Option<String>,
336    /// `ReplaceGraveyard$ <Zone>` for `PlayEffect`: install a one-shot
337    /// replacement that reroutes the played card to `<Zone>` if it would
338    /// otherwise be put into the graveyard from the stack.
339    pub replace_graveyard: Option<String>,
340    pub two_colors: bool,
341    pub or_colors: bool,
342    pub can_block_any: bool,
343    pub can_block_amount: Option<String>,
344    pub option_question: Option<String>,
345    pub perpetual_duration: bool,
346    pub pump_zone: Option<String>,
347    pub repeat_sub_ability: Option<String>,
348    pub repeat_check_svar: bool,
349    pub repeat_svar_compare: Option<String>,
350    pub repeat_defined: Option<String>,
351    pub repeat_present: Option<String>,
352    pub repeat_compare: Option<String>,
353    pub repeat_optional: bool,
354    pub repeat_optional_decider: Option<String>,
355    pub repeat_cards_text: Option<String>,
356    pub repeat_cards_selector: Option<CompiledSelector>,
357    pub repeat_players: Option<String>,
358    pub change_num_text: Option<String>,
359    pub change_valid: Option<String>,
360    pub zone: Option<ZoneType>,
361    pub valid_zone: Option<ZoneType>,
362    pub source_zone: Option<ZoneType>,
363    pub restrict_from_zone: Option<ZoneType>,
364    pub choice_zone: Option<ZoneType>,
365    pub with_total_cmc: Option<i32>,
366    pub with_total_power: Option<i32>,
367    pub mana_ability: bool,
368    pub type_filter: Option<String>,
369    pub valid_types_text: Option<String>,
370    pub remember_countered: bool,
371    pub remember_countered_cmc: bool,
372    pub remember_for_counter: bool,
373    pub lose_control: Option<crate::card::LoseControlCondition>,
374    pub untap_on_resolve: bool,
375    pub add_kws: Option<String>,
376    pub fallback_ability: Option<String>,
377    pub cumulative_upkeep: Option<String>,
378    pub echo: Option<String>,
379    pub exploit: bool,
380    pub strict_amount: bool,
381    pub base_power: bool,
382    pub remember_sacrificed: bool,
383    pub unimprint: bool,
384    pub drain_mana: bool,
385    pub remember_drained_mana: bool,
386    pub clear_remembered: bool,
387    pub etb: bool,
388    pub remember_tapped: bool,
389    pub untap_type: Option<String>,
390    pub untap_up_to: bool,
391    pub track_discarded: bool,
392    pub no_peek: bool,
393    pub no_reveal: bool,
394    pub remember_peeked: bool,
395    pub reveal_optional: bool,
396    pub remember_revealed: bool,
397    pub imprint_revealed: bool,
398    pub remember_found: bool,
399    pub imprint_found: bool,
400    pub peek_amount_text: Option<String>,
401    pub champion: bool,
402    pub with_noted_counters: bool,
403    pub exiled_with_effect_source: bool,
404    pub token_tapped: bool,
405    pub token_blocking_text: Option<String>,
406    pub remember_tokens: bool,
407    pub remember_original_tokens: bool,
408    pub imprint_tokens: bool,
409    pub remember_source: bool,
410    pub token_remembered: Option<String>,
411    pub cleanup_for_each: bool,
412    pub add_triggers_from_text: Option<String>,
413    pub at_eot_trig_text: Option<String>,
414    pub pump_duration_text: Option<String>,
415    pub searched: bool,
416    pub reorder: bool,
417    pub exactly: bool,
418    pub no_looking: bool,
419    pub remember_lki_flag: bool,
420    pub remember_searched: bool,
421    pub ninjutsu: bool,
422    pub unearth: bool,
423    pub attacking: bool,
424    pub cant_fizzle: bool,
425    pub cant_be_countered: bool,
426    pub unpayable: bool,
427    pub pw_ability: bool,
428    pub flash: bool,
429    pub split_second: bool,
430    pub random: bool,
431    pub random_target: bool,
432    pub rest_random_order: bool,
433    pub prompt_to_skip_optional_ability: bool,
434    pub prompt_if_only_possible_ability: bool,
435    pub skip_cancel_prompt: bool,
436    pub temp_remember: bool,
437    pub num_random_choices: Option<String>,
438    pub num_dmg_text: Option<String>,
439    pub num_cards_text: Option<String>,
440    pub sides: Option<i32>,
441    pub result_sub_abilities_text: Option<String>,
442    pub result_svar_text: Option<String>,
443    pub chosen_svar_text: Option<String>,
444    pub other_svar_text: Option<String>,
445    pub spellbook_text: Option<String>,
446    pub dungeon_text: Option<String>,
447    pub var_name_text: Option<String>,
448    pub animate_power: Option<i32>,
449    pub animate_toughness: Option<i32>,
450    pub animate_types_text: Option<String>,
451    pub animate_keywords_text: Option<String>,
452    pub animate_remove_keywords_text: Option<String>,
453    pub animate_triggers_text: Option<String>,
454    pub animate_colors_text: Option<String>,
455    pub overwrite_colors: bool,
456    pub animate_overwrite_types: bool,
457    pub animate_remove_creature_types: bool,
458    pub animate_remove_all_abilities: bool,
459    pub animate_incorporate_text: Option<String>,
460    pub animate_mana_cost_override_text: Option<String>,
461    pub sneak: bool,
462    pub attacking_text: Option<String>,
463    pub ninjutsu_text: Option<String>,
464    pub token_attacking_text: Option<String>,
465    pub without_mana_cost: bool,
466    pub cast_from_play_effect: bool,
467    pub store_vote_num: bool,
468    pub remember_voted_objects: bool,
469    pub remember_played: bool,
470    pub discard_valid_text: Option<String>,
471    pub discard_valid_selector: Option<CompiledSelector>,
472    pub reveal_valid_text: Option<String>,
473    pub reveal_valid_selector: Option<CompiledSelector>,
474    pub any_number: bool,
475    pub remember_discarded: bool,
476    pub allows_paying_with_shard: bool,
477    pub cant_be_copied_ability: bool,
478    pub mana_replacement: Option<String>,
479    pub remember_manifested: bool,
480    pub unless_payer_text: Option<String>,
481    pub unless_resolve_subs: Option<String>,
482    pub unless_switched: bool,
483    pub vote_message_text: Option<String>,
484    pub optional_decider_text: Option<String>,
485    pub stack_description_text: Option<String>,
486    pub sp_desc_text: Option<String>,
487    pub spell_description_text: Option<String>,
488    pub condition_description_text: Option<String>,
489    pub after_description_text: Option<String>,
490    pub announce_text: Option<String>,
491    pub x_max_limit_text: Option<String>,
492    pub announce_max_text: Option<String>,
493    pub optional_ability_prompt: Option<String>,
494    pub svar_name_text: Option<String>,
495    pub svar_type_text: Option<String>,
496    pub svar_expression_text: Option<String>,
497    pub change_color_word_text: Option<String>,
498    pub change_type_word_text: Option<String>,
499    pub forbidden_new_types_text: Option<String>,
500    pub cost_has_x: bool,
501    pub targeting_player: bool,
502    pub targeting_player_text: Option<String>,
503    pub effect_source: bool,
504    pub defined_magnet_text: Option<String>,
505    pub targets_with_defined_controller_text: Option<String>,
506    pub replace_dying_exiled_with: bool,
507    pub mode: Option<SpellAbilityMode>,
508    pub duration: Option<AbilityDuration>,
509    pub replace_dying_condition: Option<ReplaceDyingCondition>,
510    pub day_time_value: Option<DayTimeValue>,
511    pub choice_restriction_text: Option<String>,
512    pub phase_text: Option<String>,
513    pub step_text: Option<String>,
514    pub phase_in_or_out_text: Option<String>,
515    pub found_destination_zone: Option<ZoneType>,
516    pub revealed_destination_zone: Option<ZoneType>,
517    pub extra_phase_text: Option<String>,
518    pub card_state_name: Option<String>,
519    pub track_mana_spent: bool,
520    pub become_starting_player: bool,
521    pub tap_creatures_for_mana: bool,
522    pub remember_players_text: Option<String>,
523    pub no_call: bool,
524    pub flip_until_you_lose: bool,
525    pub different_names: bool,
526    pub different_cmc: bool,
527    pub different_power: bool,
528    pub share_land_type: bool,
529    pub imprint_last: bool,
530    pub foretold: bool,
531    pub foretold_cost: bool,
532    pub random_order: bool,
533    pub shuffle_changed_pile: bool,
534    pub warp: bool,
535    pub morph: bool,
536    pub morph_up: bool,
537    pub megamorph: bool,
538    pub mega: bool,
539    pub imprint_made: bool,
540    pub to_visit_your_attractions: bool,
541    pub remember_highest_player: bool,
542    pub use_highest_roll: bool,
543    pub use_difference_between_rolls: bool,
544    pub store_results: bool,
545    pub even_odd_results: bool,
546    pub different_results: bool,
547    pub max_rolls_results: bool,
548    pub note_doubles: bool,
549    pub subs_for_each: bool,
550    pub reroll_results: bool,
551    pub semantic_numeric_params: BTreeMap<String, NumericParamIr>,
552}
553
554impl SpellAbilityIr {
555    pub fn from_parsed(
556        api: Option<crate::ability::api_type::ApiType>,
557        params: &ParsedParams<'_>,
558    ) -> Self {
559        Self {
560            card_trait_requirements: CardTraitRequirementsIr::from_parsed(params),
561            effect: lower_effect_ir(api, params),
562            mode_text: params.get(keys::MODE).map(str::to_string),
563            discard_mode: params
564                .get(keys::MODE)
565                .and_then(crate::ability::effects::discard_effect::DiscardMode::parse),
566            produced_ir: params
567                .semantic_get(keys::PRODUCED)
568                .and_then(|param| match param.value {
569                    SemanticParamValue::ProducedMana(produced) => {
570                        Some(ProducedMana::from_semantic(&produced))
571                    }
572                    _ => None,
573                }),
574            reflect_property: params.get(keys::REFLECT_PROPERTY).map(str::to_string),
575            color_or_type: params.get(keys::COLOR_OR_TYPE).map(str::to_string),
576            restrict_valid: params.get(keys::RESTRICT_VALID).map(str::to_string),
577            valid_filter_text: params.get(keys::VALID).map(str::to_string),
578            valid_filter_selector: params.get(keys::VALID).map(CompiledSelector::parse),
579            adds_keywords: params.get(keys::ADDS_KEYWORDS).map(str::to_string),
580            adds_keywords_valid: params.get(keys::ADDS_KEYWORDS_VALID).map(str::to_string),
581            adds_counters: params.get(keys::ADDS_COUNTERS).map(str::to_string),
582            adds_counters_valid: params.get(keys::ADDS_COUNTERS_VALID).map(str::to_string),
583            triggers_when_spent: params.get(keys::TRIGGERS_WHEN_SPENT).map(str::to_string),
584            valid_cards_text: params.get(keys::VALID_CARDS).map(str::to_string),
585            valid_cards_selector: params.get(keys::VALID_CARDS).map(CompiledSelector::parse),
586            valid_card_text: params.get(keys::VALID_CARD).map(str::to_string),
587            valid_card_selector: params.get(keys::VALID_CARD).map(CompiledSelector::parse),
588            valid_players_text: params.get(keys::VALID_PLAYERS).map(str::to_string),
589            valid_players_selector: params.get(keys::VALID_PLAYERS).map(CompiledSelector::parse),
590            valid_player_text: params.get(keys::VALID_PLAYER).map(str::to_string),
591            valid_player_selector: params.get(keys::VALID_PLAYER).map(CompiledSelector::parse),
592            valid_tgts_text: params.get(keys::VALID_TGTS).map(str::to_string),
593            valid_tgts_selector: params.get(keys::VALID_TGTS).map(CompiledSelector::parse),
594            valid_target_text: params.get(keys::VALID_TARGET).map(str::to_string),
595            valid_target_selector: params.get(keys::VALID_TARGET).map(CompiledSelector::parse),
596            all_valid_text: params.get("AllValid").map(str::to_string),
597            all_valid_selector: params.get("AllValid").map(CompiledSelector::parse),
598            defined_name_text: params.get(keys::DEFINED_NAME).map(str::to_string),
599            set_chosen_number: params.get("SetChosenNumber").map(str::to_string),
600            starting_with: params.get("StartingWith").map(str::to_string),
601            repeat: params.get(keys::REPEAT).map(str::to_string),
602            branch_condition_svar: params.get(keys::BRANCH_CONDITION_SVAR).map(str::to_string),
603            branch_condition_svar_compare: params
604                .get(keys::BRANCH_CONDITION_SVAR_COMPARE)
605                .map(str::to_string),
606            true_sub_ability: params.get(keys::TRUE_SUB_ABILITY).map(str::to_string),
607            false_sub_ability: params.get(keys::FALSE_SUB_ABILITY).map(str::to_string),
608            play_cost_text: params.get(keys::PLAY_COST).map(str::to_string),
609            unless_cost: params.get(keys::UNLESS_COST).map(str::to_string),
610            amount: params.get(keys::AMOUNT).map(str::to_string),
611            counter_num_text: params.get("CounterNum").map(str::to_string),
612            max_repeat: params.get("MaxRepeat").map(str::to_string),
613            precost_desc: params.get(keys::PRECOST_DESC).map(str::to_string),
614            token_amount: params.get("TokenAmount").map(str::to_string),
615            sac_valid: params.get(keys::SAC_VALID).map(str::to_string),
616            defined: params.get(keys::DEFINED).map(DefinedExpr::parse),
617            defined_player: params.get(keys::DEFINED_PLAYER).map(DefinedExpr::parse),
618            defined_text: params.get(keys::DEFINED).map(str::to_string),
619            defined_player_text: params.get(keys::DEFINED_PLAYER).map(str::to_string),
620            controller_text: params.get(keys::CONTROLLER).map(str::to_string),
621            effect_owner: params.get(keys::EFFECT_OWNER).map(DefinedExpr::parse),
622            effect_owner_text: params.get(keys::EFFECT_OWNER).map(str::to_string),
623            origin_text: params.get(keys::ORIGIN).map(str::to_string),
624            origin_zone: parsed_zone_type(params.get(keys::ORIGIN)),
625            origin_zones: parsed_zone_types(params.get(keys::ORIGIN)),
626            destination_text: params
627                .get(keys::DESTINATION_ZONE)
628                .or_else(|| params.get(keys::DESTINATION))
629                .map(str::to_string),
630            destination_zone: parsed_zone_type(
631                params
632                    .get(keys::DESTINATION_ZONE)
633                    .or_else(|| params.get(keys::DESTINATION)),
634            ),
635            destination_zone_2: parsed_zone_type(params.get(keys::DESTINATION_ZONE_2)),
636            zone1: parsed_zone_type(params.get(keys::ZONE1)),
637            zone2: parsed_zone_type(params.get(keys::ZONE2)),
638            change_type: params.get(keys::CHANGE_TYPE).map(str::to_string),
639            change_type_selector: params.get(keys::CHANGE_TYPE).map(CompiledSelector::parse),
640            primary_text: params.get(keys::PRIMARY).map(str::to_string),
641            secondary_text: params.get(keys::SECONDARY).map(str::to_string),
642            secondary_type_text: params.get(keys::SECONDARY_TYPE).map(str::to_string),
643            library_position: params.get(keys::LIBRARY_POSITION).map(str::to_string),
644            library_position_2: params.get(keys::LIBRARY_POSITION_2).map(str::to_string),
645            library_position_alternative: params
646                .get(keys::LIBRARY_POSITION_ALTERNATIVE)
647                .map(str::to_string),
648            shuffle_raw: params.get(keys::SHUFFLE).map(str::to_string),
649            no_shuffle: parsed_true(params.get(keys::NO_SHUFFLE)),
650            mandatory: parsed_true(params.get(keys::MANDATORY)),
651            toggle: parsed_true(params.get(keys::TOGGLE)),
652            can_repeat_modes: params.has(keys::CAN_REPEAT_MODES),
653            entwine: params.has(keys::ENTWINE),
654            skip_untap: params.has(keys::SKIP_UNTAP),
655            tapped: parsed_true(params.get(keys::TAPPED)),
656            shuffle: parsed_true(params.get(keys::SHUFFLE)),
657            optional: params.has(keys::OPTIONAL),
658            gain_control: params.has(keys::GAIN_CONTROL),
659            gain_control_text: params.get(keys::GAIN_CONTROL).map(str::to_string),
660            forget_changed: parsed_true(params.get(keys::FORGET_CHANGED)),
661            adds_no_counter: parsed_true(params.get(keys::ADDS_NO_COUNTER)),
662            no_regen: params.has("NoRegen"),
663            damage_map: params.has(keys::DAMAGE_MAP),
664            remember_damaged_creature: parsed_true(params.get(keys::REMEMBER_DAMAGED_CREATURE)),
665            num_dmg_present: params.has(keys::NUM_DMG),
666            boon: params.has("Boon"),
667            unique: params.has("Unique"),
668            exile_on_moved: params.has("ExileOnMoved"),
669            forget_on_phased_in: params.has("ForgetOnPhasedIn"),
670            forget_counter: params.has("ForgetCounter"),
671            radiance: params.has("Radiance"),
672            chooser: params.get(keys::CHOOSER).map(str::to_string),
673            gains: params.get(keys::GAINS).map(str::to_string),
674            choices: params.get(keys::CHOICES).map(str::to_string),
675            choices_selector: params.get(keys::CHOICES).map(CompiledSelector::parse),
676            debuff: DebuffIr::from_parsed(params),
677            for_each_text: params.get(keys::FOR_EACH).map(str::to_string),
678            exclude: params.get("Exclude").map(str::to_string),
679            object_text: params.get(keys::OBJECT).map(str::to_string),
680            source_text: params.get(keys::SOURCE).map(str::to_string),
681            attached_to: params.get(keys::ATTACHED_TO).map(str::to_string),
682            attached_to_player_text: params.get(keys::ATTACHED_TO_PLAYER).map(str::to_string),
683            attach_after_text: params.get(keys::ATTACH_AFTER).map(str::to_string),
684            destination_alternative: params
685                .get(keys::DESTINATION_ALTERNATIVE)
686                .map(str::to_string),
687            select_prompt: params.get(keys::SELECT_PROMPT).map(str::to_string),
688            sub_ability_name: params.get(keys::SUB_ABILITY).map(str::to_string),
689            name_text: params.get(keys::NAME).map(str::to_string),
690            names_text: params.get(keys::NAMES).map(str::to_string),
691            choose_from_list_text: params.get(keys::CHOOSE_FROM_LIST).map(str::to_string),
692            choose_from_defined_cards: params.has(keys::CHOOSE_FROM_DEFINED_CARDS),
693            stack_id: params
694                .get(keys::STACK_ID)
695                .and_then(|value| value.parse().ok()),
696            token_script: params.get(keys::TOKEN_SCRIPT).map(str::to_string),
697            token_owner: params.get(keys::TOKEN_OWNER).map(str::to_string),
698            token_name_text: params.get(keys::TOKEN_NAME).map(str::to_string),
699            token_power: parsed_i32(params.get(keys::TOKEN_POWER)),
700            token_toughness: parsed_i32(params.get(keys::TOKEN_TOUGHNESS)),
701            token_types_text: params.get(keys::TOKEN_TYPES).map(str::to_string),
702            token_colors_text: params.get(keys::TOKEN_COLORS).map(str::to_string),
703            token_keywords_text: params.get(keys::TOKEN_KEYWORDS).map(str::to_string),
704            token_until_text: params.get("TokenUntil").map(str::to_string),
705            keyword_text: params.get("Keyword").map(str::to_string),
706            with_counters_type_text: params.get(keys::WITH_COUNTERS_TYPE).map(str::to_string),
707            with_counters_type: params.get(keys::WITH_COUNTERS_TYPE).map(parse_counter_type),
708            with_counters_amount_text: params.get(keys::WITH_COUNTERS_AMOUNT).map(str::to_string),
709            change_num: parsed_usize(params.get(keys::CHANGE_NUM)).unwrap_or(1),
710            hidden: parsed_true(params.get(keys::HIDDEN)),
711            skip_reorder: parsed_true(params.get("SkipReorder")),
712            secretly: parsed_true(params.get(keys::SECRETLY)),
713            random_chosen: parsed_true(params.get(keys::RANDOM_CHOSEN)),
714            forget_other_remembered: parsed_true(params.get(keys::FORGET_OTHER_REMEMBERED)),
715            remember_changed: params.has(keys::REMEMBER_CHANGED),
716            imprint: parsed_true(params.get(keys::IMPRINT)),
717            face_down: params.has(keys::FACE_DOWN),
718            exile_face_down: parsed_true(params.get(keys::EXILE_FACE_DOWN)),
719            transformed: parsed_true(params.get(keys::TRANSFORMED)),
720            at_random_text: params.get(keys::AT_RANDOM).map(str::to_string),
721            at_random: params.has(keys::AT_RANDOM),
722            reveal: !parsed_true(params.get(keys::NO_REVEAL)),
723            reveal_true: parsed_true(params.get(keys::REVEAL)),
724            reveal_text: params.get(keys::REVEAL).map(str::to_string),
725            remember_drawn: params.has("RememberDrawn"),
726            remember_removed_cards: parsed_true(params.get(keys::REMEMBER_REMOVED_CARDS)),
727            remember_destroyed: parsed_true(params.get("RememberDestroyed")),
728            remember_abandoned: params.get("RememberAbandoned").is_some(),
729            may_shuffle: params.has(keys::MAY_SHUFFLE),
730            remember_altered: parsed_true(params.get(keys::REMEMBER_ALTERED)),
731            alter_attribute_activate: parsed_bool_default(params.get(keys::ACTIVATE), true),
732            alter_attribute_attributes: split_param_list_value(params.get(keys::ATTRIBUTES), ","),
733            remember_amass: parsed_true(params.get(keys::REMEMBER_AMASS)),
734            remember_flag: params.has(keys::REMEMBER),
735            remember_chosen: parsed_true(params.get(keys::REMEMBER_CHOSEN)),
736            imprint_chosen: parsed_true(params.get(keys::IMPRINT_CHOSEN)),
737            remember_clasher: parsed_true(params.get(keys::REMEMBER_CLASHER)),
738            remember_cloaked: parsed_true(params.get(keys::REMEMBER_CLOAKED)),
739            remember_discovered: parsed_true(params.get(keys::REMEMBER_DISCOVERED)),
740            remember_drafted: parsed_true(params.get(keys::REMEMBER_DRAFTED)),
741            remember_exchanged: parsed_true(params.get(keys::REMEMBER_EXCHANGED)),
742            remember_investigating_players: parsed_true(
743                params.get(keys::REMEMBER_INVESTIGATING_PLAYERS),
744            ),
745            remember_made: parsed_true(params.get(keys::REMEMBER_MADE)),
746            always_remember: parsed_true(params.get("AlwaysRemember")),
747            remember_milled: params.has("RememberMilled"),
748            change_zone_table: params.has("ChangeZoneTable"),
749            at_eot: params.get(keys::AT_EOT).map(str::to_string),
750            execute: params.get(keys::EXECUTE).map(str::to_string),
751            replace_with_text: params.get(keys::REPLACE_WITH).map(str::to_string),
752            damage_amount_text: params.get(keys::DAMAGE_AMOUNT).map(str::to_string),
753            counter_type_text: params.get(keys::COUNTER_TYPE).map(str::to_string),
754            counter_type: params.get(keys::COUNTER_TYPE).map(parse_counter_type),
755            simple_counter_type_choice_path: params.get(keys::COUNTER_TYPE).is_some()
756                && ![
757                    "EachExistingCounter",
758                    "EachFromSource",
759                    "UniqueType",
760                    "CounterTypePerDefined",
761                    "CounterTypes",
762                    "ChooseDifferent",
763                    "PutOnEachOther",
764                    "PutOnDefined",
765                    "TriggeredCounterMap",
766                    "SharedKeywords",
767                ]
768                .iter()
769                .any(|key| params.has(key)),
770            modular: parsed_true(params.get(keys::MODULAR)),
771            adapt: parsed_true(params.get(keys::ADAPT)),
772            monstrosity: parsed_true(params.get(keys::MONSTROSITY)),
773            bloodthirst: parsed_true(params.get("Bloodthirst")),
774            renown: parsed_true(params.get(keys::RENOWN)),
775            abilities: params.get("Abilities").map(str::to_string),
776            replacement_effects: params.get("ReplacementEffects").map(str::to_string),
777            static_abilities: params.get(keys::STATIC_ABILITIES).map(str::to_string),
778            triggers: params.get(keys::TRIGGERS).map(str::to_string),
779            imprint_cards: params.get("ImprintCards").map(str::to_string),
780            remember_spell: params.get("RememberSpell").map(str::to_string),
781            remember_lki: params.get("RememberLKI").map(str::to_string),
782            remember_targets: params.has("RememberTargets"),
783            forget_other_targets: params.has("ForgetOtherTargets"),
784            remember_cost_mana: params.has("RememberCostMana"),
785            forget_on_moved_text: params.get(keys::FORGET_ON_MOVED).map(str::to_string),
786            forget_on_moved_zone: parsed_zone_type(params.get(keys::FORGET_ON_MOVED)),
787            replace_dying_defined: params.get("ReplaceDyingDefined").map(DefinedExpr::parse),
788            replace_dying_defined_text: params.get("ReplaceDyingDefined").map(str::to_string),
789            replace_dying_valid: params.get("ReplaceDyingValid").map(str::to_string),
790            replace_dying_zone_text: params.get("ReplaceDyingZone").map(str::to_string),
791            replace_dying_zone: parsed_zone_type(params.get("ReplaceDyingZone")),
792            remember_objects: params.get(keys::REMEMBER_OBJECTS).map(str::to_string),
793            remember_objects_remembered: has_remember_objects_token(params, "Remembered"),
794            remember_objects_remembered_controller: has_remember_objects_token(
795                params,
796                "RememberedController",
797            ),
798            remember_objects_remembered_lki: has_remember_objects_token(params, "RememberedLKI"),
799            remember_objects_triggered_attacker_lki_copy: has_remember_objects_token(
800                params,
801                "TriggeredAttackerLKICopy",
802            ),
803            remember_number: params.has(keys::REMEMBER_NUMBER),
804            remember_svar_amount: params.get(keys::REMEMBER_SVAR_AMOUNT).map(str::to_string),
805            remember_exiled: params.has("RememberExiled"),
806            delayed_trigger_defined_player: params
807                .get(keys::DELAYED_TRIGGER_DEFINED_PLAYER)
808                .map(DefinedExpr::parse),
809            delayed_trigger_defined_player_text: params
810                .get(keys::DELAYED_TRIGGER_DEFINED_PLAYER)
811                .map(str::to_string),
812            this_turn: params.has("ThisTurn"),
813            condition_hellbent: parsed_true(params.get("ConditionHellbent")),
814            condition_threshold: parsed_true(params.get("ConditionThreshold")),
815            condition_metalcraft: parsed_true(params.get("ConditionMetalcraft")),
816            condition_delirium: parsed_true(params.get("ConditionDelirium")),
817            condition_revolt: parsed_true(params.get("ConditionRevolt")),
818            condition_desert: parsed_true(params.get("ConditionDesert")),
819            condition_blessing: parsed_true(params.get("ConditionBlessing")),
820            condition_kicked: parsed_true(params.get("ConditionKicked")),
821            condition_optional_paid: parsed_true(params.get("ConditionOptionalPaid")),
822            condition_optional_not_paid: parsed_true(params.get("ConditionOptionalNotPaid")),
823            condition_opponent_turn: parsed_true(params.get("ConditionOpponentTurn")),
824            condition_player_turn: params.get("ConditionPlayerTurn").map(str::to_string),
825            condition_cards_in_hand: params.get("ConditionCardsInHand").map(str::to_string),
826            condition_phases: params.get("ConditionPhases").map(str::to_string),
827            condition_life_compare: params.get("ConditionLifeCompare").map(str::to_string),
828            condition: params.get(keys::CONDITION).map(str::to_string),
829            condition_check_svar: params.get(keys::CONDITION_CHECK_SVAR).map(str::to_string),
830            condition_svar_compare: params.get("ConditionSVarCompare").map(str::to_string),
831            condition_present: params.get(keys::CONDITION_PRESENT).map(str::to_string),
832            condition_defined: params.get(keys::CONDITION_DEFINED).map(DefinedExpr::parse),
833            condition_defined_text: params.get(keys::CONDITION_DEFINED).map(str::to_string),
834            condition_compare: params.get(keys::CONDITION_COMPARE).map(str::to_string),
835            condition_zone_text: params.get(keys::CONDITION_ZONE).map(str::to_string),
836            condition_zone: parsed_zone_type(params.get(keys::CONDITION_ZONE)),
837            optional_present: params.has(keys::OPTIONAL),
838            remove_from_combat: parsed_true(params.get(keys::REMOVE_FROM_COMBAT)),
839            num_att: params.get(keys::NUM_ATT).map(str::to_string),
840            num_def: params.get(keys::NUM_DEF).map(str::to_string),
841            types_text: params
842                .get(keys::TYPES)
843                .or_else(|| params.get(keys::TYPE))
844                .map(str::to_string),
845            add_types: params.get(keys::ADD_TYPES).map(str::to_string),
846            set_color: params.get(keys::SET_COLOR).map(str::to_string),
847            set_power: params.get(keys::SET_POWER).map(str::to_string),
848            set_toughness: params.get(keys::SET_TOUGHNESS).map(str::to_string),
849            set_mana_cost: params.get(keys::SET_MANA_COST).map(str::to_string),
850            clone_target: params.get(keys::CLONE_TARGET).map(str::to_string),
851            kw: params.get(keys::KW).map(str::to_string),
852            pump_keywords: params.get(keys::PUMP_KEYWORDS).map(str::to_string),
853            add_keywords: params.get(keys::ADD_KEYWORDS).map(str::to_string),
854            kw_choice: params.get("KWChoice").map(str::to_string),
855            up_to: params.has("UpTo"),
856            upto: params.has(keys::UPTO),
857            optional_decider: params.get(keys::OPTIONAL_DECIDER).map(str::to_string),
858            replace_graveyard: params.get(keys::REPLACE_GRAVEYARD).map(str::to_string),
859            two_colors: params.has("TwoColors"),
860            or_colors: params.has("OrColors"),
861            can_block_any: parsed_true(params.get("CanBlockAny")),
862            can_block_amount: params.get("CanBlockAmount").map(str::to_string),
863            option_question: params.get("OptionQuestion").map(str::to_string),
864            perpetual_duration: matches!(
865                params.get(keys::DURATION).map(AbilityDuration::parse),
866                Some(AbilityDuration::Perpetual)
867            ),
868            pump_zone: params.get("PumpZone").map(str::to_string),
869            repeat_sub_ability: params.get(keys::REPEAT_SUB_ABILITY).map(str::to_string),
870            repeat_check_svar: params.has("RepeatCheckSVar"),
871            repeat_svar_compare: params.get("RepeatSVarCompare").map(str::to_string),
872            repeat_defined: params.get("RepeatDefined").map(str::to_string),
873            repeat_present: params.get("RepeatPresent").map(str::to_string),
874            repeat_compare: params.get("RepeatCompare").map(str::to_string),
875            repeat_optional: params.has("RepeatOptional"),
876            repeat_optional_decider: params.get("RepeatOptionalDecider").map(str::to_string),
877            repeat_cards_text: params.get(keys::REPEAT_CARDS).map(str::to_string),
878            repeat_cards_selector: params.get(keys::REPEAT_CARDS).map(CompiledSelector::parse),
879            repeat_players: params.get(keys::REPEAT_PLAYERS).map(str::to_string),
880            change_num_text: params.get(keys::CHANGE_NUM).map(str::to_string),
881            change_valid: params.get(keys::CHANGE_VALID).map(str::to_string),
882            zone: parsed_zone_type(params.get(keys::ZONE)),
883            valid_zone: parsed_zone_type(params.get(keys::VALID_ZONE)),
884            source_zone: parsed_zone_type(params.get("SourceZone")),
885            restrict_from_zone: parsed_zone_type(params.get(keys::RESTRICT_FROM_ZONE)),
886            choice_zone: parsed_zone_type(params.get(keys::CHOICE_ZONE)),
887            with_total_cmc: parsed_i32(params.get(keys::WITH_TOTAL_CMC)),
888            with_total_power: parsed_i32(params.get(keys::WITH_TOTAL_POWER)),
889            mana_ability: parsed_true(params.get(keys::MANA_ABILITY)),
890            type_filter: params.get(keys::TYPE).map(str::to_string),
891            valid_types_text: params.get(keys::VALID_TYPES).map(str::to_string),
892            remember_countered: params.has(keys::REMEMBER_COUNTERED),
893            remember_countered_cmc: params.has(keys::REMEMBER_COUNTERED_CMC),
894            remember_for_counter: params.has(keys::REMEMBER_FOR_COUNTER),
895            lose_control: params
896                .get(keys::LOSE_CONTROL)
897                .and_then(|raw| raw.parse::<crate::card::LoseControlCondition>().ok()),
898            untap_on_resolve: params.has(keys::UNTAP),
899            add_kws: params.get(keys::ADD_KWS).map(str::to_string),
900            fallback_ability: params.get("FallbackAbility").map(str::to_string),
901            cumulative_upkeep: params.get(keys::CUMULATIVE_UPKEEP).map(str::to_string),
902            echo: params.get("Echo").map(str::to_string),
903            exploit: parsed_true(params.get(keys::EXPLOIT)),
904            strict_amount: params.has(keys::STRICT_AMOUNT),
905            base_power: params.get("BasePower").is_some(),
906            remember_sacrificed: parsed_true(params.get(keys::REMEMBER_SACRIFICED)),
907            unimprint: parsed_true(params.get(keys::UNIMPRINT)),
908            drain_mana: parsed_true(params.get(keys::DRAIN_MANA)),
909            remember_drained_mana: parsed_true(params.get(keys::REMEMBER_DRAINED_MANA)),
910            clear_remembered: parsed_true(params.get("ClearRemembered")),
911            etb: params.has(keys::ETB),
912            remember_tapped: params.has(keys::REMEMBER_TAPPED),
913            untap_type: params.get("UntapType").map(str::to_string),
914            untap_up_to: parsed_true(params.get("UntapUpTo")),
915            track_discarded: parsed_true(params.get(keys::TRACK_DISCARDED)),
916            no_peek: parsed_true(params.get("NoPeek")),
917            no_reveal: parsed_true(params.get("NoReveal")),
918            remember_peeked: parsed_true(params.get("RememberPeeked")),
919            reveal_optional: parsed_true(params.get("RevealOptional")),
920            remember_revealed: parsed_true(params.get("RememberRevealed")),
921            imprint_revealed: parsed_true(params.get("ImprintRevealed")),
922            remember_found: parsed_true(params.get(keys::REMEMBER_FOUND)),
923            imprint_found: parsed_true(params.get(keys::IMPRINT_FOUND)),
924            peek_amount_text: params.get("PeekAmount").map(str::to_string),
925            champion: parsed_true(params.get(keys::CHAMPION)),
926            with_noted_counters: parsed_true(params.get(keys::WITH_NOTED_COUNTERS)),
927            exiled_with_effect_source: params.has("ExiledWithEffectSource"),
928            token_tapped: params.has(keys::TOKEN_TAPPED),
929            token_blocking_text: params.get(keys::TOKEN_BLOCKING).map(str::to_string),
930            remember_tokens: params.has(keys::REMEMBER_TOKENS),
931            remember_original_tokens: params.has(keys::REMEMBER_ORIGINAL_TOKENS),
932            imprint_tokens: params.has(keys::IMPRINT_TOKENS),
933            remember_source: params.has(keys::REMEMBER_SOURCE),
934            token_remembered: params.get(keys::TOKEN_REMEMBERED).map(str::to_string),
935            cleanup_for_each: params.has(keys::CLEANUP_FOR_EACH),
936            add_triggers_from_text: params.get(keys::ADD_TRIGGERS_FROM).map(str::to_string),
937            at_eot_trig_text: params.get(keys::AT_EOT_TRIG).map(str::to_string),
938            pump_duration_text: params.get(keys::PUMP_DURATION).map(str::to_string),
939            searched: parsed_true(params.get(keys::SEARCHED)),
940            reorder: parsed_true(params.get("Reorder")),
941            exactly: parsed_true(params.get(keys::EXACTLY)),
942            no_looking: parsed_true(params.get(keys::NO_LOOKING)),
943            remember_lki_flag: parsed_true(params.get("RememberLKI")),
944            remember_searched: parsed_true(params.get(keys::REMEMBER_SEARCHED)),
945            ninjutsu: parsed_true(params.get(keys::NINJUTSU)),
946            unearth: parsed_true(params.get(keys::UNEARTH)),
947            attacking: params.has(keys::ATTACKING),
948            cant_fizzle: params.has(keys::CANT_FIZZLE),
949            cant_be_countered: parsed_true(params.get("CantBeCountered")),
950            unpayable: parsed_true(params.get("Unpayable")),
951            pw_ability: parsed_true(params.get(keys::PLANESWALKER))
952                || parsed_true(params.get("PwAbility")),
953            flash: parsed_true(params.get("Flash")),
954            split_second: parsed_true(params.get("SplitSecond")),
955            random: parsed_true(params.get("Random")),
956            random_target: parsed_true(params.get(keys::RANDOM_TARGET)),
957            rest_random_order: parsed_true(params.get("RestRandomOrder")),
958            prompt_to_skip_optional_ability: params.has(keys::PROMPT_TO_SKIP_OPTIONAL_ABILITY),
959            prompt_if_only_possible_ability: parsed_true(params.get("PromptIfOnlyPossible")),
960            skip_cancel_prompt: parsed_true(params.get("SkipCancelPrompt")),
961            temp_remember: params.has("TempRemember"),
962            num_random_choices: params.get("NumRandomChoices").map(str::to_string),
963            num_dmg_text: params.get("NumDmg").map(str::to_string),
964            num_cards_text: params.get("NumCards").map(str::to_string),
965            sides: parsed_i32(params.get(keys::SIDES)),
966            result_sub_abilities_text: params.get(keys::RESULT_SUB_ABILITIES).map(str::to_string),
967            result_svar_text: params.get("ResultSVar").map(str::to_string),
968            chosen_svar_text: params.get("ChosenSVar").map(str::to_string),
969            other_svar_text: params.get("OtherSVar").map(str::to_string),
970            spellbook_text: params.get(keys::SPELLBOOK).map(str::to_string),
971            dungeon_text: params.get(keys::DUNGEON).map(str::to_string),
972            var_name_text: params.get(keys::VAR_NAME).map(str::to_string),
973            animate_power: parsed_i32(params.get(keys::POWER)),
974            animate_toughness: parsed_i32(params.get(keys::TOUGHNESS)),
975            animate_types_text: params.get(keys::TYPES).map(str::to_string),
976            animate_keywords_text: params.get(keys::KEYWORDS).map(str::to_string),
977            animate_remove_keywords_text: params.get("RemoveKeywords").map(str::to_string),
978            animate_triggers_text: params.get(keys::TRIGGERS).map(str::to_string),
979            animate_colors_text: params.get("Colors").map(str::to_string),
980            overwrite_colors: parsed_true(params.get(keys::OVERWRITE_COLORS)),
981            animate_overwrite_types: parsed_true(params.get("OverwriteTypes")),
982            animate_remove_creature_types: parsed_true(params.get(keys::REMOVE_CREATURE_TYPES)),
983            animate_remove_all_abilities: parsed_true(params.get(keys::REMOVE_ALL_ABILITIES)),
984            animate_incorporate_text: params.get("Incorporate").map(str::to_string),
985            animate_mana_cost_override_text: params.get("ManaCost").map(str::to_string),
986            sneak: parsed_true(params.get(keys::SNEAK)),
987            attacking_text: params.get(keys::ATTACKING).map(str::to_string),
988            ninjutsu_text: params.get(keys::NINJUTSU).map(str::to_string),
989            token_attacking_text: params.get(keys::TOKEN_ATTACKING).map(str::to_string),
990            without_mana_cost: params.has("WithoutManaCost"),
991            cast_from_play_effect: params.has("CastFromPlayEffect"),
992            store_vote_num: parsed_true(params.get(keys::STORE_VOTE_NUM)),
993            remember_voted_objects: parsed_true(params.get(keys::REMEMBER_VOTED_OBJECTS)),
994            remember_played: parsed_true(params.get("RememberPlayed")),
995            discard_valid_text: params.get("DiscardValid").map(str::to_string),
996            discard_valid_selector: params.get("DiscardValid").map(CompiledSelector::parse),
997            reveal_valid_text: params.get("RevealValid").map(str::to_string),
998            reveal_valid_selector: params.get("RevealValid").map(CompiledSelector::parse),
999            any_number: parsed_true(params.get("AnyNumber")),
1000            remember_discarded: parsed_true(params.get("RememberDiscarded")),
1001            allows_paying_with_shard: parsed_true(params.get("AllowsPayingWithShard")),
1002            cant_be_copied_ability: parsed_true(params.get("CantBeCopied")),
1003            mana_replacement: params.get(keys::MANA_REPLACEMENT).map(str::to_string),
1004            remember_manifested: parsed_true(params.get(keys::REMEMBER_MANIFESTED)),
1005            unless_payer_text: params.get(keys::UNLESS_PAYER).map(str::to_string),
1006            unless_resolve_subs: params.get("UnlessResolveSubs").map(str::to_string),
1007            unless_switched: params.has(keys::UNLESS_SWITCHED),
1008            vote_message_text: params.get(keys::VOTE_MESSAGE).map(str::to_string),
1009            optional_decider_text: params.get(keys::OPTIONAL_DECIDER).map(str::to_string),
1010            stack_description_text: params.get("StackDescription").map(str::to_string),
1011            sp_desc_text: params.get("SpDesc").map(str::to_string),
1012            spell_description_text: params.get("SpellDescription").map(str::to_string),
1013            condition_description_text: params.get("ConditionDescription").map(str::to_string),
1014            after_description_text: params.get("AfterDescription").map(str::to_string),
1015            announce_text: params.get("Announce").map(str::to_string),
1016            x_max_limit_text: params.get("XMaxLimit").map(str::to_string),
1017            announce_max_text: params.get("AnnounceMax").map(str::to_string),
1018            optional_ability_prompt: params
1019                .get(keys::OPTIONAL_ABILITY_PROMPT)
1020                .map(str::to_string),
1021            svar_name_text: params
1022                .get("SVar")
1023                .or_else(|| params.get(keys::SVAR_NAME))
1024                .map(str::to_string),
1025            svar_type_text: params.get("Type").map(str::to_string),
1026            svar_expression_text: params
1027                .get("Expression")
1028                .or_else(|| params.get(keys::SVAR_VALUE))
1029                .map(str::to_string),
1030            change_color_word_text: params.get(keys::CHANGE_COLOR_WORD).map(str::to_string),
1031            change_type_word_text: params.get(keys::CHANGE_TYPE_WORD).map(str::to_string),
1032            forbidden_new_types_text: params.get(keys::FORBIDDEN_NEW_TYPES).map(str::to_string),
1033            cost_has_x: params
1034                .get(keys::COST)
1035                .is_some_and(|cost| cost.contains('X')),
1036            targeting_player: params.has(keys::TARGETING_PLAYER),
1037            targeting_player_text: params.get(keys::TARGETING_PLAYER).map(str::to_string),
1038            effect_source: params.has(keys::EFFECT_SOURCE),
1039            defined_magnet_text: params.get(keys::DEFINED_MAGNET).map(str::to_string),
1040            targets_with_defined_controller_text: params
1041                .get("TargetsWithDefinedController")
1042                .map(str::to_string),
1043            replace_dying_exiled_with: params.has("ReplaceDyingExiledWith"),
1044            mode: params.get(keys::MODE).map(SpellAbilityMode::parse),
1045            duration: params.get(keys::DURATION).map(AbilityDuration::parse),
1046            replace_dying_condition: params
1047                .get("ReplaceDyingCondition")
1048                .map(ReplaceDyingCondition::parse),
1049            day_time_value: params.get(keys::VALUE).and_then(DayTimeValue::parse),
1050            choice_restriction_text: params.get("ChoiceRestriction").map(str::to_string),
1051            phase_text: params.get(keys::PHASE).map(str::to_string),
1052            step_text: params.get(keys::STEP).map(str::to_string),
1053            phase_in_or_out_text: params.get("PhaseInOrOut").map(str::to_string),
1054            found_destination_zone: parsed_zone_type(params.get("FoundDestination")),
1055            revealed_destination_zone: parsed_zone_type(params.get("RevealedDestination")),
1056            extra_phase_text: params.get("ExtraPhase").map(str::to_string),
1057            card_state_name: params.get("CardState").map(str::to_string),
1058            track_mana_spent: parsed_true(params.get("TrackManaSpent")),
1059            become_starting_player: params.has("BecomeStartingPlayer"),
1060            tap_creatures_for_mana: params.has("TapCreaturesForMana"),
1061            remember_players_text: params.get(keys::REMEMBER_PLAYERS).map(str::to_string),
1062            no_call: parsed_true(params.get("NoCall")),
1063            flip_until_you_lose: parsed_true(params.get("FlipUntilYouLose")),
1064            different_names: parsed_true(params.get(keys::DIFFERENT_NAMES)),
1065            different_cmc: parsed_true(params.get(keys::DIFFERENT_CMC)),
1066            different_power: parsed_true(params.get(keys::DIFFERENT_POWER)),
1067            share_land_type: parsed_true(params.get(keys::SHARE_LAND_TYPE)),
1068            imprint_last: parsed_true(params.get(keys::IMPRINT_LAST)),
1069            foretold: parsed_true(params.get(keys::FORETOLD)),
1070            foretold_cost: parsed_true(params.get(keys::FORETOLD_COST)),
1071            random_order: parsed_true(params.get(keys::RANDOM_ORDER)),
1072            shuffle_changed_pile: parsed_true(params.get(keys::SHUFFLE_CHANGED_PILE)),
1073            warp: params.has(keys::WARP),
1074            morph: parsed_true(params.get("Morph")),
1075            morph_up: parsed_true(params.get("MorphUp")),
1076            megamorph: parsed_true(params.get("Megamorph")),
1077            mega: parsed_true(params.get(keys::MEGA)),
1078            imprint_made: parsed_true(params.get(keys::IMPRINT_MADE)),
1079            to_visit_your_attractions: parsed_true(params.get("ToVisitYourAttractions")),
1080            remember_highest_player: parsed_true(params.get("RememberHighestPlayer")),
1081            use_highest_roll: parsed_true(params.get("UseHighestRoll")),
1082            use_difference_between_rolls: parsed_true(params.get("UseDifferenceBetweenRolls")),
1083            store_results: parsed_true(params.get("StoreResults")),
1084            even_odd_results: parsed_true(params.get("EvenOddResults")),
1085            different_results: parsed_true(params.get("DifferentResults")),
1086            max_rolls_results: parsed_true(params.get("MaxRollsResults")),
1087            note_doubles: parsed_true(params.get("NoteDoubles")),
1088            subs_for_each: parsed_true(params.get("SubsForEach")),
1089            reroll_results: parsed_true(params.get("RerollResults")),
1090            semantic_numeric_params: BTreeMap::new(),
1091        }
1092    }
1093
1094    pub fn compile_numeric_params_from_runtime(&mut self, params: &Params) {
1095        self.semantic_numeric_params = params
1096            .iter()
1097            .filter_map(|(key, value)| {
1098                NumericParamIr::from_semantic(&parse_semantic_param_value(key, value))
1099                    .map(|compiled| (key.to_string(), compiled))
1100            })
1101            .collect();
1102    }
1103}
1104
1105#[derive(Debug, Clone, PartialEq, Eq)]
1106pub struct DefinedExpr {
1107    pub refs: Vec<DefinedRef>,
1108}
1109
1110impl DefinedExpr {
1111    pub fn parse(raw: &str) -> Self {
1112        Self {
1113            refs: raw
1114                .split(" & ")
1115                .map(str::trim)
1116                .filter(|part| !part.is_empty())
1117                .map(DefinedRef::parse)
1118                .collect(),
1119        }
1120    }
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq, strum_macros::AsRefStr, strum_macros::EnumString)]
1124pub enum DefinedRef {
1125    #[strum(serialize = "Self", serialize = "CARDNAME")]
1126    SelfCard,
1127    You,
1128    Opponent,
1129    Player,
1130    Players,
1131    Targeted,
1132    TargetedCard,
1133    ThisTargetedCard,
1134    TargetedPlayer,
1135    ThisTargetedPlayer,
1136    TargetedOrController,
1137    TargetedController,
1138    ThisTargetedController,
1139    ParentTargetedController,
1140    TargetedOwner,
1141    ThisTargetedOwner,
1142    ParentTarget,
1143    ParentTargeted,
1144    TriggeredCard,
1145    #[strum(serialize = "TriggeredCardLKI")]
1146    TriggeredCardLki,
1147    #[strum(serialize = "TriggeredCardLKICopy")]
1148    TriggeredCardLkiCopy,
1149    TriggeredPlayer,
1150    TriggeredSource,
1151    TriggeredNewCard,
1152    #[strum(serialize = "TriggeredNewCardLKICopy")]
1153    TriggeredNewCardLkiCopy,
1154    TriggeredAttacker,
1155    TriggeredAttackers,
1156    TriggeredBlocker,
1157    TriggeredTarget,
1158    #[strum(serialize = "TriggeredTargetLKICopy")]
1159    TriggeredTargetLkiCopy,
1160    TriggeredTargets,
1161    TriggeredTargetController,
1162    TriggeredTargetsController,
1163    TriggeredAttackerController,
1164    TriggeredBlockerController,
1165    DefendingPlayer,
1166    TriggeredDefendingPlayer,
1167    ReplacedCard,
1168    #[strum(serialize = "ReplacedCardLKI")]
1169    ReplacedCardLki,
1170    Remembered,
1171    #[strum(serialize = "RememberedLKI")]
1172    RememberedLki,
1173    DelayTriggerRemembered,
1174    #[strum(serialize = "DelayTriggerRememberedLKI")]
1175    DelayTriggerRememberedLki,
1176    TriggerRemembered,
1177    Imprinted,
1178    ExiledWith,
1179    Explorer,
1180    Explored,
1181    Discarded,
1182    Sacrificed,
1183    #[strum(default)]
1184    Unsupported(String),
1185}
1186
1187impl DefinedRef {
1188    pub fn parse(raw: &str) -> Self {
1189        raw.parse()
1190            .unwrap_or_else(|_| Self::Unsupported(raw.to_string()))
1191    }
1192
1193    pub fn is_supported(&self) -> bool {
1194        !matches!(self, Self::Unsupported(_))
1195    }
1196
1197    pub fn as_legacy_str(&self) -> &str {
1198        match self {
1199            Self::SelfCard => "Self",
1200            Self::Unsupported(raw) => raw,
1201            other => other.as_ref(),
1202        }
1203    }
1204}
1205
1206#[derive(Debug, Clone, PartialEq, Eq)]
1207pub enum NumericParamIr {
1208    Integer(i32),
1209    Amount(AmountExpr),
1210    SVarReference(Vec<String>),
1211    Raw(String),
1212}
1213
1214impl NumericParamIr {
1215    fn from_semantic(value: &SemanticParamValue<'_>) -> Option<Self> {
1216        match value {
1217            SemanticParamValue::Integer(value) => Some(Self::Integer(*value)),
1218            SemanticParamValue::Amount(amount) => {
1219                Some(Self::Amount(AmountExpr::from_semantic(amount)))
1220            }
1221            SemanticParamValue::SVarReference(names) => Some(Self::SVarReference(
1222                names.iter().map(|name| (*name).to_string()).collect(),
1223            )),
1224            SemanticParamValue::Raw(raw)
1225            | SemanticParamValue::Expression(raw)
1226            | SemanticParamValue::Text(raw)
1227            | SemanticParamValue::Symbol(raw) => Some(Self::Raw((*raw).to_string())),
1228            _ => None,
1229        }
1230    }
1231}
1232
1233#[derive(Debug, Clone, PartialEq, Eq)]
1234pub struct DealDamageIr {
1235    pub amount: Option<AmountExpr>,
1236    pub valid_targets: Option<String>,
1237    pub damage_map: bool,
1238}
1239
1240impl DealDamageIr {
1241    pub fn from_parsed(params: &ParsedParams<'_>) -> Self {
1242        Self {
1243            amount: semantic_amount_expr(params, keys::NUM_DMG),
1244            valid_targets: params.get(keys::VALID_TGTS).map(str::to_string),
1245            damage_map: params.has(keys::DAMAGE_MAP),
1246        }
1247    }
1248}
1249
1250#[derive(Debug, Clone, PartialEq, Eq)]
1251pub struct NumericAmountIr {
1252    pub amount: Option<AmountExpr>,
1253}
1254
1255impl NumericAmountIr {
1256    pub fn from_parsed(params: &ParsedParams<'_>, key: &str) -> Self {
1257        Self {
1258            amount: semantic_amount_expr(params, key),
1259        }
1260    }
1261}
1262
1263fn semantic_amount_expr(params: &ParsedParams<'_>, key: &str) -> Option<AmountExpr> {
1264    let param = params.semantic_get(key)?;
1265    match param.value {
1266        SemanticParamValue::Amount(amount) => Some(AmountExpr::from_semantic(&amount)),
1267        SemanticParamValue::Integer(value) => Some(AmountExpr::Literal(value)),
1268        SemanticParamValue::Raw(raw)
1269        | SemanticParamValue::Expression(raw)
1270        | SemanticParamValue::Text(raw)
1271        | SemanticParamValue::Symbol(raw) => Some(AmountExpr::parse(raw)),
1272        _ => param
1273            .raw_value
1274            .is_empty()
1275            .then_some(AmountExpr::Raw(String::new())),
1276    }
1277}
1278
1279pub fn lower_effect_ir(
1280    api: Option<crate::ability::api_type::ApiType>,
1281    params: &ParsedParams<'_>,
1282) -> Option<EffectIr> {
1283    match api {
1284        Some(crate::ability::api_type::ApiType::DamageAll) => Some(EffectIr::DamageAll(
1285            NumericAmountIr::from_parsed(params, keys::NUM_DMG),
1286        )),
1287        Some(crate::ability::api_type::ApiType::DealDamage) => {
1288            Some(EffectIr::DealDamage(DealDamageIr::from_parsed(params)))
1289        }
1290        Some(crate::ability::api_type::ApiType::Draw) => Some(EffectIr::Draw(
1291            NumericAmountIr::from_parsed(params, keys::NUM_CARDS),
1292        )),
1293        Some(crate::ability::api_type::ApiType::GainLife) => Some(EffectIr::GainLife(
1294            NumericAmountIr::from_parsed(params, keys::LIFE_AMOUNT),
1295        )),
1296        Some(crate::ability::api_type::ApiType::SetLife) => Some(EffectIr::LifeSet(
1297            NumericAmountIr::from_parsed(params, keys::LIFE_AMOUNT),
1298        )),
1299        Some(crate::ability::api_type::ApiType::LoseLife) => Some(EffectIr::LoseLife(
1300            NumericAmountIr::from_parsed(params, keys::LIFE_AMOUNT),
1301        )),
1302        Some(crate::ability::api_type::ApiType::Mill) => Some(EffectIr::Mill(
1303            NumericAmountIr::from_parsed(params, keys::NUM_CARDS),
1304        )),
1305        Some(crate::ability::api_type::ApiType::Poison) => Some(EffectIr::Poison(
1306            NumericAmountIr::from_parsed(params, keys::NUM),
1307        )),
1308        _ => None,
1309    }
1310}
1311
1312fn parsed_zone_type(value: Option<&str>) -> Option<ZoneType> {
1313    let value = value?.trim();
1314    if value.eq_ignore_ascii_case("Deck") {
1315        Some(ZoneType::Library)
1316    } else {
1317        ZoneType::from_str_compat(value)
1318    }
1319}
1320
1321fn parsed_zone_types(value: Option<&str>) -> Vec<ZoneType> {
1322    value
1323        .map(|zones| {
1324            zones
1325                .split([',', ' '])
1326                .map(str::trim)
1327                .filter(|zone| !zone.is_empty())
1328                .filter_map(|zone| {
1329                    if zone.eq_ignore_ascii_case("Deck") {
1330                        Some(ZoneType::Library)
1331                    } else {
1332                        ZoneType::from_str_compat(zone)
1333                    }
1334                })
1335                .collect()
1336        })
1337        .unwrap_or_default()
1338}
1339
1340fn parsed_i32(value: Option<&str>) -> Option<i32> {
1341    value?.parse().ok()
1342}
1343
1344fn parsed_usize(value: Option<&str>) -> Option<usize> {
1345    value?.parse().ok()
1346}
1347
1348fn parsed_bool_default(value: Option<&str>, default: bool) -> bool {
1349    value
1350        .map(|value| value.eq_ignore_ascii_case("True"))
1351        .unwrap_or(default)
1352}
1353
1354fn parsed_true(value: Option<&str>) -> bool {
1355    value.is_some_and(|value| value.eq_ignore_ascii_case("True"))
1356}
1357
1358fn has_remember_objects_token(params: &ParsedParams<'_>, token: &str) -> bool {
1359    params
1360        .get(keys::REMEMBER_OBJECTS)
1361        .is_some_and(|raw| raw.split(" & ").any(|t| t.trim() == token))
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::*;
1367    use crate::parsing::ParsedParams;
1368
1369    #[test]
1370    fn planeswalker_param_marks_pw_ability() {
1371        let params = ParsedParams::parse("AB$ Discard | Planeswalker$ True");
1372        let ir = SpellAbilityIr::from_parsed(None, &params);
1373        assert!(ir.pw_ability);
1374    }
1375}