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