Skip to main content

manabrew_engine/card/
keyword_gen.rs

1//! Keyword-based ability and trigger generation for Card.
2//!
3//! These functions translate keywords like "Cycling", "Prowess", "Bushido", etc. into
4//! concrete activated abilities and triggered abilities. They're called during card
5//! initialization in `Card::from_rules()`.
6
7use crate::ability::activated::parse_activated_ability;
8use crate::card::svar_cache::ParsedSVarKind;
9use crate::parsing::keys;
10use crate::parsing::Params;
11use crate::staticability::parse_static_ability;
12use crate::trigger::parse_trigger;
13
14use super::Card;
15
16impl Card {
17    fn parsed_svar_params(&mut self, name: &str) -> Option<Params> {
18        match self.parsed_s_var(name)?.kind {
19            ParsedSVarKind::Ability { params, .. } | ParsedSVarKind::ParamRecord { params } => {
20                Some(params)
21            }
22            ParsedSVarKind::Number { .. }
23            | ParsedSVarKind::Count { .. }
24            | ParsedSVarKind::NumericExpression { .. }
25            | ParsedSVarKind::Raw { .. } => None,
26        }
27    }
28
29    /// Generate intrinsic mana abilities for basic land subtypes (Plains → {W}, etc.).
30    /// Mirrors Java's `CardFactoryUtil.addIntrinsicAbilities()`.
31    pub(crate) fn generate_basic_land_mana_abilities(&mut self) {
32        const SUBTYPE_MANA: &[(&str, &str, &str)] = &[
33            ("Plains", "W", "Add {W}."),
34            ("Island", "U", "Add {U}."),
35            ("Swamp", "B", "Add {B}."),
36            ("Mountain", "R", "Add {R}."),
37            ("Forest", "G", "Add {G}."),
38        ];
39        for &(subtype, letter, desc) in SUBTYPE_MANA {
40            if self.type_line.has_subtype(subtype) {
41                let already_produces = self.activated_abilities.iter().any(|ab| {
42                    ab.is_mana_ability
43                        && ab
44                            .produced_ir
45                            .as_ref()
46                            .is_some_and(|ir| ir.as_script_text() == letter)
47                });
48                if !already_produces {
49                    let raw = format!(
50                        "AB$ Mana | Cost$ T | Produced$ {} | SpellDescription$ {}",
51                        letter, desc
52                    );
53                    let idx = self.abilities.len();
54                    self.abilities.push(raw.clone());
55                    if let Some(ab) = parse_activated_ability(&raw, idx) {
56                        self.activated_abilities.push(ab);
57                    }
58                }
59            }
60        }
61    }
62
63    /// Generate activated abilities from keywords (e.g. Cycling → AB$ Draw).
64    /// Mirrors Java's `CardFactoryUtil.setupKeywordedAbilities()`.
65    pub(super) fn generate_keyword_abilities(&mut self) {
66        // Cycling: K:Cycling:{cost} → AB$ Draw | Cost$ {cost} Discard<1/CARDNAME> | ActivationZone$ Hand
67        if let Some(cycling_cost) = self.get_keyword_cost("Cycling") {
68            let ab_text = format!(
69                "AB$ Draw | Cost$ {} Discard<1/CARDNAME> | ActivationZone$ Hand | NumCards$ 1 | Defined$ You",
70                cycling_cost
71            );
72            let next_idx = self.activated_abilities.len();
73            if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
74                self.activated_abilities.push(ab);
75            }
76        }
77
78        // TypeCycling: K:TypeCycling:{type}:{cost} → AB$ ChangeZone | Cost$ {cost} Discard<1/CARDNAME> | ActivationZone$ Hand
79        // Mirrors Java CardFactoryUtil lines 3852-3864.
80        for kw in self
81            .keywords
82            .iter_strings()
83            .chain(self.granted_keywords.iter_strings())
84        {
85            if let Some(rest) = kw.strip_prefix("TypeCycling:") {
86                let parts: Vec<&str> = rest.splitn(2, ':').collect();
87                if parts.len() == 2 {
88                    let cycle_type = parts[0].trim(); // e.g., "Swamp"
89                    let mana_cost = parts[1].trim(); // e.g., "1"
90                                                     // getTitleWithoutCost() = capitalize(descType) + "cycling"
91                    let precost_desc = format!(
92                        "{}cycling",
93                        cycle_type
94                            .chars()
95                            .next()
96                            .map(|c| c.to_uppercase().to_string())
97                            .unwrap_or_default()
98                            + &cycle_type[1..]
99                    );
100                    let ab_text = format!(
101                        "AB$ ChangeZone | Cost$ {} Discard<1/CARDNAME> | ActivationZone$ Hand | PrecostDesc$ {} | Origin$ Library | Destination$ Hand | ChangeType$ {}",
102                        mana_cost, precost_desc, cycle_type
103                    );
104                    let next_idx = self.activated_abilities.len();
105                    if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
106                        self.activated_abilities.push(ab);
107                    }
108                }
109            }
110        }
111
112        // Equip: K:Equip:{cost}[...]
113        // Forge keyword payload can include optional suffix data; we only need
114        // the activation cost + default target filter to mirror Java baseline.
115        for equip_raw in self
116            .keywords
117            .iter_strings()
118            .chain(self.granted_keywords.iter_strings())
119            .filter_map(|kw| crate::keyword::extract_keyword_cost_str(kw, "Equip"))
120        {
121            let payload = equip_raw.split(":::").next().unwrap_or(equip_raw).trim();
122            let mut parts = payload.split(':');
123            let equip_cost = parts.next().unwrap_or(payload).trim();
124            let target_filter = parts
125                .next()
126                .map(str::trim)
127                .filter(|s| !s.is_empty())
128                .unwrap_or("Creature.YouCtrl");
129            if !equip_cost.is_empty() {
130                let ab_text = format!(
131                    "AB$ Attach | Cost$ {} | ValidTgts$ {} | SorcerySpeed$ True | SpellDescription$ Equip {}",
132                    equip_cost, target_filter, equip_cost
133                );
134                let next_idx = self.activated_abilities.len();
135                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
136                    self.activated_abilities.push(ab);
137                }
138            }
139        }
140
141        // Adapt: K:Adapt:N:cost → AB$ PutCounter with Adapt$ True gate.
142        // Mirrors Java CardFactoryUtil lines 2665-2684.
143        for kw in self
144            .keywords
145            .iter_strings()
146            .chain(self.granted_keywords.iter_strings())
147        {
148            if let Some(rest) = crate::keyword::extract_keyword_cost_str(kw, "Adapt") {
149                let parts: Vec<&str> = rest.splitn(2, ':').collect();
150                if parts.len() == 2 {
151                    let magnitude = parts[0].trim();
152                    let mana_cost = parts[1].trim();
153                    let ab_text = format!(
154                        "AB$ PutCounter | Cost$ {} | Adapt$ True | CounterNum$ {} | CounterType$ P1P1 | StackDescription$ SpellDescription | SpellDescription$ Adapt {}",
155                        mana_cost, magnitude, magnitude
156                    );
157                    let next_idx = self.activated_abilities.len();
158                    if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
159                        self.activated_abilities.push(ab);
160                    }
161                }
162            }
163        }
164
165        // Crew: K:Crew:N → AB$ Animate (tap creatures with total power ≥N).
166        // Mirrors Java CardFactoryUtil lines 3820-3835.
167        // Uses tapXType<Any/Creature.Other+withTotalPowerGE{N}> matching Java's format.
168        for kw in self
169            .keywords
170            .iter_strings()
171            .chain(self.granted_keywords.iter_strings())
172        {
173            if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Crew") {
174                let n = n_str.trim();
175                let ab_text = format!(
176                    "AB$ Animate | Cost$ tapXType<Any/Creature.Other+withTotalPowerGE{{{}}}> | Defined$ Self | Types$ Artifact,Creature | Secondary$ True | SpellDescription$ Crew {}",
177                    n, n
178                );
179                let next_idx = self.activated_abilities.len();
180                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
181                    self.activated_abilities.push(ab);
182                }
183            }
184        }
185
186        // Station: K:Station:N → AB$ PutCounter (tap another creature to add charge counters).
187        // Mirrors Java CardFactoryUtil lines 3587-3595.
188        // The ability is sorcery-speed and puts charge counters equal to the tapped
189        // creature's power onto this Spacecraft/Planet.
190        for kw in self
191            .keywords
192            .iter_strings()
193            .chain(self.granted_keywords.iter_strings())
194        {
195            if let Some(_n_str) = crate::keyword::extract_keyword_cost_str(kw, "Station") {
196                let ab_text = "AB$ PutCounter | Cost$ tapXType<1/Creature.Other> | Defined$ Self | CounterType$ CHARGE | CounterNum$ StationX | SorcerySpeed$ True | CostDesc$ | SpellDescription$ Station";
197                let next_idx = self.activated_abilities.len();
198                if let Some(ab) = parse_activated_ability(ab_text, next_idx) {
199                    self.activated_abilities.push(ab);
200                }
201                self.svars
202                    .entry("StationX".to_string())
203                    .or_insert_with(|| "TappedCards$TapPowerValue".to_string());
204            }
205        }
206
207        // Embalm: K:Embalm:cost → AB$ CopyPermanent from graveyard.
208        // Mirrors Java CardFactoryUtil lines 2879-2891.
209        for kw in self
210            .keywords
211            .iter_strings()
212            .chain(self.granted_keywords.iter_strings())
213        {
214            if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(kw, "Embalm") {
215                let cost = cost_str.trim();
216                let ab_text = format!(
217                    "AB$ CopyPermanent | Cost$ {} ExileFromGrave<1/CARDNAME> | ActivationZone$ Graveyard | SorcerySpeed$ True | Defined$ Self | SetColor$ White | AddTypes$ Zombie | SpellDescription$ Embalm",
218                    cost
219                );
220                let next_idx = self.activated_abilities.len();
221                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
222                    self.activated_abilities.push(ab);
223                }
224            }
225        }
226
227        // Eternalize: K:Eternalize:cost → AB$ CopyPermanent from graveyard as 4/4.
228        // Mirrors Java CardFactoryUtil lines 3023-3052.
229        for kw in self
230            .keywords
231            .iter_strings()
232            .chain(self.granted_keywords.iter_strings())
233        {
234            if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(kw, "Eternalize") {
235                let cost = cost_str.trim();
236                let ab_text = format!(
237                    "AB$ CopyPermanent | Cost$ {} ExileFromGrave<1/CARDNAME> | ActivationZone$ Graveyard | SorcerySpeed$ True | Defined$ Self | SetColor$ Black | SetPower$ 4 | SetToughness$ 4 | AddTypes$ Zombie | SpellDescription$ Eternalize",
238                    cost
239                );
240                let next_idx = self.activated_abilities.len();
241                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
242                    self.activated_abilities.push(ab);
243                }
244            }
245        }
246
247        // Enlist: K:Enlist -> intrinsic optional attack cost static ability.
248        if self
249            .keywords
250            .iter_strings()
251            .chain(self.granted_keywords.iter_strings())
252            .any(|k| k.eq_ignore_ascii_case("Enlist"))
253        {
254            let raw = "S:Mode$ OptionalAttackCost | ValidCard$ Card.Self | Cost$ Enlist<1/CARDNAME/creature> | Secondary$ True | Trigger$ TrigEnlist";
255            if let Some(sa) = parse_static_ability(raw) {
256                self.add_static_ability(sa);
257            }
258            self.svars.entry("TrigEnlist".to_string()).or_insert_with(|| {
259                "DB$ Pump | NumAtt$ TriggerRemembered$CardPower | SpellDescription$ When you do, add its power to this creature's until end of turn.".to_string()
260            });
261        }
262
263        // Morph / Megamorph: mark card as castable face-down for {3}.
264        // The actual casting logic is in game_action_util (playable check + cost handling).
265        if self
266            .keywords
267            .iter_strings()
268            .chain(self.granted_keywords.iter_strings())
269            .any(|k| k.starts_with("Morph:") || k.starts_with("Megamorph:"))
270        {
271            self.has_morph = true;
272        }
273
274        // Plot: K:Plot:{cost} → AB$ Plot | Cost$ {cost} | ActivationZone$ Hand | SorcerySpeed$ True
275        // Mirrors Java CardFactoryUtil lines 3398-3449.
276        // Exiles the card from hand; plotted cards can later be cast for free.
277        if let Some(plot_cost) = self.get_keyword_cost("Plot") {
278            let ab_text = format!(
279                "AB$ Plot | Cost$ {} | ActivationZone$ Hand | SorcerySpeed$ True | Secondary$ True | SpellDescription$ Plot",
280                plot_cost
281            );
282            let next_idx = self.activated_abilities.len();
283            if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
284                self.activated_abilities.push(ab);
285            }
286        }
287
288        // Class: K:Class:{level}:{cost}:{params} → AB$ ClassLevelUp.
289        // Mirrors Java CardFactoryUtil lines 2789-2799.
290        let class_keywords: Vec<String> = self
291            .keywords
292            .iter_strings()
293            .chain(self.granted_keywords.iter_strings())
294            .filter(|kw| kw.starts_with("Class:"))
295            .map(|kw| kw.to_string())
296            .collect();
297        for kw in class_keywords {
298            if let Some(rest) = kw.strip_prefix("Class:") {
299                let mut parts = rest.splitn(3, ':');
300                let level = parts.next().unwrap_or_default().trim();
301                let cost = parts.next().unwrap_or_default().trim();
302                let params = parts.next().unwrap_or_default().trim();
303
304                let Ok(level_num) = level.parse::<i32>() else {
305                    continue;
306                };
307                if cost.is_empty() {
308                    continue;
309                }
310
311                let ab_text = format!(
312                    "AB$ ClassLevelUp | Cost$ {} | ClassLevel$ EQ{} | SorcerySpeed$ True | StackDescription$ SpellDescription | SpellDescription$ Level {}",
313                    cost,
314                    level_num - 1,
315                    level_num
316                );
317                let next_idx = self.activated_abilities.len();
318                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
319                    self.activated_abilities.push(ab);
320                }
321
322                if !params.is_empty() {
323                    let parsed = Params::from_raw(params);
324                    let mut desc_parts: Vec<String> = Vec::new();
325
326                    if let Some(add_trigger) = parsed.get("AddTrigger") {
327                        for svar_name in add_trigger
328                            .split(" & ")
329                            .map(str::trim)
330                            .filter(|s| !s.is_empty())
331                        {
332                            if let Some(svar_params) = self.parsed_svar_params(svar_name) {
333                                if let Some(desc) = svar_params.get(keys::TRIGGER_DESCRIPTION) {
334                                    desc_parts.push(desc.to_string());
335                                }
336                            }
337                        }
338                    }
339
340                    if let Some(add_static) = parsed.get("AddStaticAbility") {
341                        for svar_name in add_static
342                            .split(" & ")
343                            .map(str::trim)
344                            .filter(|s| !s.is_empty())
345                        {
346                            if let Some(svar_params) = self.parsed_svar_params(svar_name) {
347                                if let Some(desc) = svar_params.get(keys::DESCRIPTION) {
348                                    desc_parts.push(desc.to_string());
349                                }
350                            }
351                        }
352                    }
353
354                    if let Some(add_replacement) = parsed.get("AddReplacementEffect") {
355                        for svar_name in add_replacement
356                            .split(" & ")
357                            .map(str::trim)
358                            .filter(|s| !s.is_empty())
359                        {
360                            if let Some(svar_params) = self.parsed_svar_params(svar_name) {
361                                if let Some(desc) = svar_params.get(keys::DESCRIPTION) {
362                                    desc_parts.push(desc.to_string());
363                                }
364                            }
365                        }
366                    }
367
368                    let mut effect = format!(
369                        "Mode$ Continuous | Affected$ Card.Self | ClassLevel$ {} | {}",
370                        level_num, params
371                    );
372                    if !desc_parts.is_empty() {
373                        effect.push_str(" | Description$ ");
374                        effect.push_str(&desc_parts.join("\r\n"));
375                    }
376                    if let Some(st) = parse_static_ability(&effect) {
377                        self.add_static_ability(st);
378                    }
379                }
380            }
381        }
382    }
383
384    pub fn ensure_crew_activated_ability(&mut self) {
385        if self.activated_abilities.iter().any(|ab| {
386            ab.spell_description
387                .as_deref()
388                .is_some_and(|desc| desc.starts_with("Crew"))
389        }) {
390            return;
391        }
392        for kw in self.keywords.iter_strings() {
393            if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Crew") {
394                let n = n_str.trim();
395                let ab_text = format!(
396                    "AB$ Animate | Cost$ tapXType<Any/Creature.Other+withTotalPowerGE{{{}}}> | Defined$ Self | Types$ Artifact,Creature | Secondary$ True | SpellDescription$ Crew {}",
397                    n, n
398                );
399                let next_idx = self.activated_abilities.len();
400                if let Some(ab) = parse_activated_ability(&ab_text, next_idx) {
401                    self.activated_abilities.push(ab);
402                    self.base_ability_count = self.activated_abilities.len();
403                }
404                return;
405            }
406        }
407    }
408
409    /// Generate triggered abilities from keywords (e.g. Prowess, Bushido, Annihilator, etc.).
410    /// Mirrors Java's `CardFactoryUtil.setupKeywordedTriggers()`.
411    pub fn generate_keyword_triggers(&mut self) {
412        let mut next_id = self.triggers.len() as u32;
413
414        for kw in self.keywords.as_string_list() {
415            self.generate_keyword_trigger_combat(&kw, &mut next_id);
416            self.generate_keyword_trigger_zone(&kw, &mut next_id);
417            self.generate_keyword_trigger_misc(&kw, &mut next_id);
418        }
419    }
420
421    fn generate_keyword_trigger_combat(&mut self, kw: &str, next_id: &mut u32) {
422        if kw == "Prowess" {
423            let raw = "Mode$ SpellCast | ValidCard$ Card.nonCreature | ValidActivatingPlayer$ You | Execute$ TrigProwess | TriggerZones$ Battlefield | TriggerDescription$ Prowess";
424            if let Some(mut trig) = parse_trigger(raw, next_id) {
425                trig.execute = "TrigProwess".to_string();
426                self.add_trigger(trig);
427            }
428            self.svars
429                .entry("TrigProwess".to_string())
430                .or_insert_with(|| "DB$ Pump | Defined$ Self | NumAtt$ 1 | NumDef$ 1".to_string());
431        }
432
433        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Bushido") {
434            if n_str.parse::<i32>().is_ok() {
435                let raw1 = format!(
436                    "Mode$ Blocks | ValidCard$ Card.Self | Execute$ TrigBushido | TriggerZones$ Battlefield | TriggerDescription$ Bushido {n_str}"
437                );
438                if let Some(mut trig) = parse_trigger(&raw1, next_id) {
439                    trig.execute = "TrigBushido".to_string();
440                    self.add_trigger(trig);
441                }
442                let raw2 = format!(
443                    "Mode$ AttackerBlocked | ValidCard$ Card.Self | Execute$ TrigBushido | TriggerZones$ Battlefield | TriggerDescription$ Bushido {n_str}"
444                );
445                if let Some(mut trig) = parse_trigger(&raw2, next_id) {
446                    trig.execute = "TrigBushido".to_string();
447                    self.add_trigger(trig);
448                }
449                self.svars
450                    .entry("TrigBushido".to_string())
451                    .or_insert_with(|| {
452                        format!("DB$ Pump | Defined$ Self | NumAtt$ {n_str} | NumDef$ {n_str}")
453                    });
454            }
455        }
456
457        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Annihilator") {
458            if n_str.parse::<i32>().is_ok() {
459                let raw = format!(
460                    "Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigAnnihilator | TriggerZones$ Battlefield | TriggerDescription$ Annihilator {n_str}"
461                );
462                if let Some(mut trig) = parse_trigger(&raw, next_id) {
463                    trig.execute = "TrigAnnihilator".to_string();
464                    self.add_trigger(trig);
465                }
466                self.svars
467                    .entry("TrigAnnihilator".to_string())
468                    .or_insert_with(|| {
469                        format!("DB$ Sacrifice | Defined$ TriggeredDefendingPlayer | SacValid$ Permanent | Amount$ {n_str}")
470                    });
471            }
472        }
473
474        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Afflict") {
475            if n_str.parse::<i32>().is_ok() {
476                let raw = format!(
477                    "Mode$ AttackerBlocked | ValidCard$ Card.Self | TriggerZones$ Battlefield | Secondary$ True | Execute$ TrigAfflict | TriggerDescription$ Afflict {n_str}"
478                );
479                if let Some(mut trig) = parse_trigger(&raw, next_id) {
480                    trig.execute = "TrigAfflict".to_string();
481                    self.add_trigger(trig);
482                }
483                self.svars
484                    .entry("TrigAfflict".to_string())
485                    .or_insert_with(|| {
486                        format!("DB$ LoseLife | Defined$ TriggeredDefendingPlayer | LifeAmount$ {n_str}")
487                    });
488            }
489        }
490
491        if kw == "Exalted" {
492            let raw = "Mode$ Attacks | ValidCard$ Creature.YouCtrl | Alone$ True | Execute$ TrigExalted | TriggerZones$ Battlefield | TriggerDescription$ Exalted";
493            if let Some(mut trig) = parse_trigger(raw, next_id) {
494                trig.execute = "TrigExalted".to_string();
495                self.add_trigger(trig);
496            }
497            self.svars
498                .entry("TrigExalted".to_string())
499                .or_insert_with(|| {
500                    "DB$ Pump | Defined$ TriggeredAttacker | NumAtt$ +1 | NumDef$ +1".to_string()
501                });
502        }
503
504        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Renown") {
505            if n_str.parse::<i32>().is_ok() {
506                let raw = format!(
507                    "Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigRenown | TriggerZones$ Battlefield | TriggerDescription$ Renown {n_str}"
508                );
509                if let Some(mut trig) = parse_trigger(&raw, next_id) {
510                    trig.execute = "TrigRenown".to_string();
511                    self.add_trigger(trig);
512                }
513                self.svars
514                    .entry("TrigRenown".to_string())
515                    .or_insert_with(|| {
516                        format!("DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ {n_str} | Renown$ True")
517                    });
518            }
519        }
520
521        if kw == "Flanking" {
522            let raw = "Mode$ AttackerBlockedByCreature | ValidBlocked$ Card.Self | ValidCard$ Creature.withoutFlanking | Execute$ TrigFlanking | TriggerZones$ Battlefield | TriggerDescription$ Flanking";
523            if let Some(mut trig) = parse_trigger(raw, next_id) {
524                trig.execute = "TrigFlanking".to_string();
525                self.add_trigger(trig);
526            }
527            self.svars
528                .entry("TrigFlanking".to_string())
529                .or_insert_with(|| {
530                    "DB$ Pump | Defined$ TriggeredBlocker | NumAtt$ -1 | NumDef$ -1".to_string()
531                });
532        }
533
534        if kw == "Extort" {
535            let raw = "Mode$ SpellCast | ValidActivatingPlayer$ You | Execute$ TrigExtort | TriggerZones$ Battlefield | TriggerDescription$ Extort";
536            if let Some(mut trig) = parse_trigger(raw, next_id) {
537                trig.execute = "TrigExtort".to_string();
538                trig.optional = true;
539                self.add_trigger(trig);
540            }
541            self.svars.entry("TrigExtort".to_string()).or_insert_with(|| {
542                "DB$ LoseLife | Defined$ Player.Opponent | LifeAmount$ 1 | SubAbility$ ExtortGain"
543                    .to_string()
544            });
545            self.svars
546                .entry("ExtortGain".to_string())
547                .or_insert_with(|| "DB$ GainLife | Defined$ You | LifeAmount$ 1".to_string());
548        }
549    }
550
551    fn generate_keyword_trigger_zone(&mut self, kw: &str, next_id: &mut u32) {
552        self.generate_keyword_trigger_zone_graveyard(kw, next_id);
553        self.generate_keyword_trigger_zone_battlefield(kw, next_id);
554    }
555
556    fn generate_keyword_trigger_zone_graveyard(&mut self, kw: &str, next_id: &mut u32) {
557        if kw == "Undying" {
558            let raw = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self+counters_EQ0_P1P1 | TriggerZones$ Battlefield | Execute$ TrigUndying | TriggerDescription$ Undying";
559            if let Some(mut trig) = parse_trigger(raw, next_id) {
560                trig.execute = "TrigUndying".to_string();
561                self.add_trigger(trig);
562            }
563            self.svars.entry("TrigUndying".to_string()).or_insert_with(|| {
564                "DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | WithCountersType$ P1P1".to_string()
565            });
566        }
567
568        if kw == "Persist" {
569            let raw = "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self+counters_EQ0_M1M1 | TriggerZones$ Battlefield | Execute$ TrigPersist | TriggerDescription$ Persist";
570            if let Some(mut trig) = parse_trigger(raw, next_id) {
571                trig.execute = "TrigPersist".to_string();
572                self.add_trigger(trig);
573            }
574            self.svars.entry("TrigPersist".to_string()).or_insert_with(|| {
575                "DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Battlefield | WithCountersType$ M1M1".to_string()
576            });
577        }
578
579        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Afterlife") {
580            if n_str.parse::<i32>().is_ok() {
581                let raw = format!(
582                    "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigAfterlife | TriggerDescription$ Afterlife {n_str}"
583                );
584                if let Some(mut trig) = parse_trigger(&raw, next_id) {
585                    trig.execute = "TrigAfterlife".to_string();
586                    self.add_trigger(trig);
587                }
588                self.svars
589                    .entry("TrigAfterlife".to_string())
590                    .or_insert_with(|| {
591                        format!(
592                            "DB$ Token | TokenAmount$ {n_str} | TokenScript$ wb_1_1_spirit_flying"
593                        )
594                    });
595            }
596        }
597
598        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Modular") {
599            if let Ok(n) = n_str.parse::<i32>() {
600                self.etb_counters_p1p1 += n;
601
602                let raw = format!(
603                    "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigModular | TriggerDescription$ Modular {n_str}"
604                );
605                if let Some(mut trig) = parse_trigger(&raw, next_id) {
606                    trig.execute = "TrigModular".to_string();
607                    trig.optional = true;
608                    self.add_trigger(trig);
609                }
610                self.svars
611                    .entry("TrigModular".to_string())
612                    .or_insert_with(|| "SP$ Charm | Choices$ ModularMove".to_string());
613                self.svars
614                    .entry("ModularMove".to_string())
615                    .or_insert_with(|| {
616                        format!("DB$ PutCounter | Defined$ Targeted | CounterType$ P1P1 | CounterNum$ {n_str} | Modular$ true | ValidTgts$ Creature.Artifact | SpellDescription$ Put +1/+1 counter(s) on target artifact creature")
617                    });
618            }
619        }
620    }
621
622    fn generate_keyword_trigger_zone_battlefield(&mut self, kw: &str, next_id: &mut u32) {
623        if kw == "Exploit" {
624            let raw = "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExploit | TriggerDescription$ Exploit";
625            if let Some(mut trig) = parse_trigger(raw, next_id) {
626                trig.execute = "TrigExploit".to_string();
627                self.add_trigger(trig);
628            }
629            self.svars
630                .entry("TrigExploit".to_string())
631                .or_insert_with(|| {
632                    "DB$ Sacrifice | SacValid$ Creature | Optional$ True | Exploit$ True"
633                        .to_string()
634                });
635        }
636
637        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Fabricate") {
638            if n_str.parse::<i32>().is_ok() {
639                let raw = format!(
640                    "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigFabricate | Secondary$ True | TriggerDescription$ Fabricate {n_str}"
641                );
642                if let Some(mut trig) = parse_trigger(&raw, next_id) {
643                    trig.execute = "TrigFabricate".to_string();
644                    self.add_trigger(trig);
645                }
646                self.svars
647                    .entry("TrigFabricate".to_string())
648                    .or_insert_with(|| {
649                        format!(
650                            "DB$ Token | TokenAmount$ {n_str} | TokenScript$ c_1_1_a_servo \
651                             | UnlessCost$ AddCounter<{n_str}/P1P1> | UnlessPayer$ You \
652                             | SpellDescription$ Fabricate {n_str}"
653                        )
654                    });
655            }
656        }
657
658        if kw == "Living Weapon" {
659            let raw = "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Secondary$ True | TriggerDescription$ Living Weapon";
660            if let Some(mut trig) = parse_trigger(raw, next_id) {
661                trig.execute = "TrigLivingWeapon".to_string();
662                self.add_trigger(trig);
663            }
664            self.svars
665                .entry("TrigLivingWeapon".to_string())
666                .or_insert_with(|| {
667                    "DB$ Token | TokenScript$ b_0_0_phyrexian_germ | TokenOwner$ You | RememberTokens$ True | SubAbility$ DBLivingWeaponAttach".to_string()
668                });
669            self.svars
670                .entry("DBLivingWeaponAttach".to_string())
671                .or_insert_with(|| {
672                    "DB$ Attach | Defined$ Remembered | SubAbility$ DBLivingWeaponCleanup"
673                        .to_string()
674                });
675            self.svars
676                .entry("DBLivingWeaponCleanup".to_string())
677                .or_insert_with(|| "DB$ Cleanup | ClearRemembered$ True".to_string());
678        }
679
680        if let Some(n_str) = crate::keyword::extract_keyword_cost_str(kw, "Bloodthirst") {
681            if n_str.parse::<i32>().is_ok() {
682                let raw = format!(
683                    "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigBloodthirst | TriggerDescription$ Bloodthirst {n_str}"
684                );
685                if let Some(mut trig) = parse_trigger(&raw, next_id) {
686                    trig.execute = "TrigBloodthirst".to_string();
687                    self.add_trigger(trig);
688                }
689                self.svars
690                    .entry("TrigBloodthirst".to_string())
691                    .or_insert_with(|| {
692                        format!("DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ {n_str} | Bloodthirst$ True")
693                    });
694            }
695        }
696
697        if kw == "Riot" {
698            let raw = "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigRiot | TriggerDescription$ Riot";
699            if let Some(mut trig) = parse_trigger(raw, next_id) {
700                trig.execute = "TrigRiot".to_string();
701                self.add_trigger(trig);
702            }
703            self.svars
704                .entry("TrigRiot".to_string())
705                .or_insert_with(|| "SP$ Charm | Choices$ RiotCounter,RiotHaste".to_string());
706            self.svars
707                .entry("RiotCounter".to_string())
708                .or_insert_with(|| {
709                    "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on this creature".to_string()
710                });
711            self.svars
712                .entry("RiotHaste".to_string())
713                .or_insert_with(|| {
714                    "DB$ Pump | Defined$ Self | KW$ Haste | SpellDescription$ This creature gains haste".to_string()
715                });
716        }
717
718        if kw == "Unleash" {
719            let raw = "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigUnleash | TriggerDescription$ Unleash";
720            if let Some(mut trig) = parse_trigger(raw, next_id) {
721                trig.execute = "TrigUnleash".to_string();
722                self.add_trigger(trig);
723            }
724            self.svars
725                .entry("TrigUnleash".to_string())
726                .or_insert_with(|| {
727                    "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1".to_string()
728                });
729        }
730    }
731
732    fn generate_keyword_trigger_misc(&mut self, kw: &str, next_id: &mut u32) {
733        if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(kw, "Ward") {
734            let raw = "Mode$ BecomesTarget | ValidSource$ SpellAbility.OppCtrl | ValidTarget$ Card.Self | Secondary$ True | TriggerZones$ Battlefield | TriggerDescription$ Ward";
735            if let Some(mut trig) = parse_trigger(raw, next_id) {
736                trig.execute = "TrigWard".to_string();
737                self.add_trigger(trig);
738            }
739            self.svars.entry("TrigWard".to_string()).or_insert_with(|| {
740                format!("DB$ Counter | Defined$ TriggeredSourceSA | UnlessCost$ {cost_str}")
741            });
742        }
743
744        if let Some(rest) = kw.strip_prefix("Cumulative upkeep:") {
745            let cost_spec = rest.split(':').next().unwrap_or(rest);
746            let raw = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | TriggerDescription$ Cumulative upkeep";
747            if let Some(mut trig) = parse_trigger(raw, next_id) {
748                trig.execute = "TrigCumulativeUpkeep".to_string();
749                self.add_trigger(trig);
750            }
751            self.svars
752                .entry("TrigCumulativeUpkeep".to_string())
753                .or_insert_with(|| {
754                    format!("DB$ Sacrifice | SacValid$ Self | CumulativeUpkeep$ {cost_spec}")
755                });
756        }
757
758        if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(kw, "Echo") {
759            let cost_spec = cost_str.split(':').next().unwrap_or(cost_str);
760            let raw = "Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | IsPresent$ Card.Self+cameUnderControlSinceLastUpkeep | Secondary$ True | TriggerDescription$ Echo";
761            if let Some(mut trig) = parse_trigger(raw, next_id) {
762                trig.execute = "TrigEcho".to_string();
763                self.add_trigger(trig);
764            }
765            self.svars
766                .entry("TrigEcho".to_string())
767                .or_insert_with(|| format!("DB$ Sacrifice | SacValid$ Self | Echo$ {cost_spec}"));
768        }
769
770        if let Some(madness_cost) = crate::keyword::extract_keyword_cost_str(kw, "Madness") {
771            let raw = "Mode$ ChangesZone | Origin$ Hand | Destination$ Exile | ValidCard$ Card.Self | Secondary$ True | TriggerZones$ Exile | TriggerDescription$ You may cast this card for its madness cost.";
772            if let Some(mut trig) = parse_trigger(raw, next_id) {
773                trig.execute = "TrigMadnessPlay".to_string();
774                self.add_trigger(trig);
775            }
776            self.svars
777                .entry("TrigMadnessPlay".to_string())
778                .or_insert_with(|| {
779                    format!(
780                        "DB$ Play | Defined$ Self | ValidSA$ Spell | PlayCost$ {} | Optional$ True | RememberPlayed$ True | SubAbility$ MadnessMoveToYard",
781                        madness_cost
782                    )
783                });
784            self.svars
785                .entry("MadnessMoveToYard".to_string())
786                .or_insert_with(|| {
787                    "DB$ ChangeZone | Defined$ Self | Origin$ Exile | Destination$ Graveyard | TrackDiscarded$ True | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0 | SubAbility$ MadnessCleanup".to_string()
788                });
789            self.svars
790                .entry("MadnessCleanup".to_string())
791                .or_insert_with(|| "DB$ Cleanup | ClearRemembered$ True".to_string());
792        }
793
794        if let Some(partner_name) = kw.strip_prefix("Partner with:") {
795            let mut partner_name = partner_name.trim().to_string();
796            let raw = format!(
797                "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Secondary$ True | TriggerDescription$ Partner with {}",
798                partner_name
799            );
800            if let Some(mut trig) = parse_trigger(&raw, next_id) {
801                trig.execute = "TrigPartnerWith".to_string();
802                self.add_trigger(trig);
803            }
804            partner_name = partner_name.replace(',', ";");
805            self.svars
806                .entry("TrigPartnerWith".to_string())
807                .or_insert_with(|| {
808                    format!(
809                        "DB$ ChangeZone | ValidTgts$ Player | Origin$ Library | Destination$ Hand | ChangeType$ Card.named{} | Hidden$ True | Chooser$ Targeted | Optional$ True",
810                        partner_name
811                    )
812                });
813        }
814    }
815}