1use 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 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 pub(super) fn generate_keyword_abilities(&mut self) {
66 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 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(); let mana_cost = parts[1].trim(); 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 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 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 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 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 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 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 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 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 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 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 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}