1use std::collections::HashMap;
8
9use crate::ability::api_type::ApiType;
10use crate::card::Card;
11use crate::cost::parse_cost;
12use crate::cost::{Cost, CostPart};
13use crate::game::GameState;
14use crate::ids::{CardId, PlayerId};
15use crate::parsing::keys::ST;
16use crate::parsing::{keys, Params, ParsedParams};
17use crate::spellability::target_restrictions::TargetRestrictions;
18use crate::spellability::{AbilityManaPart, SpellAbility, TargetChoices};
19use forge_foundation::ZoneType;
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum AbilityRecordType {
26 Ability,
28 #[default]
30 Spell,
31 StaticAbility,
33 SubAbility,
35}
36
37impl AbilityRecordType {
38 pub fn prefix(&self) -> &'static str {
40 match self {
41 AbilityRecordType::Ability => "AB",
42 AbilityRecordType::Spell => "SP",
43 AbilityRecordType::StaticAbility => "ST",
44 AbilityRecordType::SubAbility => "DB",
45 }
46 }
47
48 pub fn from_params(params: &Params) -> Option<AbilityRecordType> {
50 if params.has(keys::AB) {
51 Some(AbilityRecordType::Ability)
52 } else if params.has(keys::SP) {
53 Some(AbilityRecordType::Spell)
54 } else if params.has(ST) {
55 Some(AbilityRecordType::StaticAbility)
56 } else if params.has(keys::DB) {
57 Some(AbilityRecordType::SubAbility)
58 } else {
59 None
60 }
61 }
62
63 pub fn from_raw(raw: &str) -> Option<AbilityRecordType> {
66 if crate::parsing::raw_has_key(raw, keys::AB) {
67 Some(AbilityRecordType::Ability)
68 } else if crate::parsing::raw_has_key(raw, keys::SP) {
69 Some(AbilityRecordType::Spell)
70 } else if crate::parsing::raw_has_key(raw, ST) {
71 Some(AbilityRecordType::StaticAbility)
72 } else if crate::parsing::raw_has_key(raw, keys::DB) {
73 Some(AbilityRecordType::SubAbility)
74 } else {
75 None
76 }
77 }
78
79 pub fn from_parsed(params: &ParsedParams<'_>) -> Option<AbilityRecordType> {
80 if params.has(keys::AB) {
81 Some(AbilityRecordType::Ability)
82 } else if params.has(keys::SP) {
83 Some(AbilityRecordType::Spell)
84 } else if params.has(ST) {
85 Some(AbilityRecordType::StaticAbility)
86 } else if params.has(keys::DB) {
87 Some(AbilityRecordType::SubAbility)
88 } else {
89 None
90 }
91 }
92
93 pub fn get_record_type(params: &Params) -> Option<AbilityRecordType> {
96 Self::from_params(params)
97 }
98
99 pub fn api_type_of<'a>(&self, params: &'a Params) -> Option<&'a str> {
101 params.get(self.prefix())
102 }
103
104 pub fn get_api_type_of(&self, params: &Params) -> Option<crate::ability::api_type::ApiType> {
107 self.api_type_of(params)
108 .and_then(crate::ability::api_type::ApiType::smart_value_of)
109 }
110}
111
112pub fn get_ability(
115 host: &crate::card::Card,
116 ability_text: &str,
117 player: crate::ids::PlayerId,
118) -> crate::spellability::SpellAbility {
119 build_spell_ability_from_host_card(host, ability_text, player)
120}
121
122pub const ADDITIONAL_ABILITY_KEYS: &[&str] = &[
125 "WinSubAbility",
126 "OtherwiseSubAbility",
127 "BidSubAbility",
128 "ChooseNumberSubAbility",
129 "Lowest",
130 "Highest",
131 "NotLowest",
132 "GuessCorrect",
133 "GuessWrong",
134 "MatchedAbility",
135 "UnmatchedAbility",
136 "HeadsSubAbility",
137 "TailsSubAbility",
138 "LoseSubAbility",
139 "TrueSubAbility",
140 "FalseSubAbility",
141 "ChosenPile",
142 "UnchosenPile",
143 "RepeatSubAbility",
144 "Execute",
145 "FallbackAbility",
146 "ChooseSubAbility",
147 "CantChooseSubAbility",
148 "RegenerationAbility",
149 "ReturnAbility",
150 "GiftAbility",
151 "VoteSubAbility",
152 "VoteTiedAbility",
153];
154
155const MAX_SUB_ABILITY_CHAIN_DEPTH: usize = 50;
156
157thread_local! {
158 static SUB_ABILITY_CHAIN_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
159}
160
161const RESTRICTION_KEYS: &[&str] = &[
162 "Activation",
163 "ActivationZone",
164 "ActivationPhases",
165 "SorcerySpeed",
166 "InstantSpeed",
167 "Activator",
168 "PlayerTurn",
169 "OpponentTurn",
170 "ActivationLimit",
171 "GameActivationLimit",
172 "Threshold",
173 "Metalcraft",
174 "Delirium",
175 "Hellbent",
176 "Revolt",
177 "Desert",
178 "Blessing",
179 "Solved",
180 "IsPresent",
181 "PresentCompare",
182 "PresentZone",
183 "PresentDefined",
184 "ClassLevel",
185 "ActivateCardsInHand",
186];
187
188const CONDITION_KEYS: &[&str] = &[
189 "ConditionPhases",
190 "ConditionPlayerTurn",
191 "ConditionOpponentTurn",
192 "ConditionThreshold",
193 "ConditionMetalcraft",
194 "ConditionDelirium",
195 "ConditionHellbent",
196 "ConditionRevolt",
197 "ConditionDesert",
198 "ConditionBlessing",
199 "ConditionSolved",
200 "ConditionPresent",
201 "ConditionCompare",
202 "ConditionPresentZone",
203 "ConditionDefined",
204];
205
206pub fn get_map_params(ab_string: &str) -> HashMap<String, String> {
209 let mut map = HashMap::new();
210 for segment in ab_string.split('|') {
211 let segment = segment.trim();
212 if let Some(idx) = segment.find('$') {
213 let key = segment[..idx].trim().to_string();
214 let value = segment[idx + 1..].trim().to_string();
215 map.insert(key, value);
216 }
217 }
218 map
219}
220
221pub fn build_spell_ability(
225 game: &GameState,
226 card_id: CardId,
227 ability_text: &str,
228 player: PlayerId,
229) -> SpellAbility {
230 let host = game.card(card_id);
231 build_spell_ability_from_host_card(host, ability_text, player)
232}
233
234pub fn build_spell_ability_from_host_card(
239 host: &Card,
240 ability_text: &str,
241 player: PlayerId,
242) -> SpellAbility {
243 let _perf_scope =
244 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::AbilityBuild);
245 crate::perf::increment_params_parse();
246 let parsed = ParsedParams::parse(ability_text);
247 let record_type = AbilityRecordType::from_parsed(&parsed).unwrap_or_else(|| {
248 panic!(
249 "AbilityFactory::build_spell_ability requires AB$/SP$/ST$/DB$ ability text; got: {ability_text:?}"
250 )
251 });
252 let params = Params::from_parsed(&parsed);
253 build_spell_ability_of_type_with_params(
254 host,
255 ability_text,
256 player,
257 record_type,
258 &parsed,
259 params,
260 )
261}
262
263pub fn build_spell_ability_for_card_cast(
269 game: &GameState,
270 card_id: CardId,
271 player: PlayerId,
272) -> SpellAbility {
273 let _perf_scope =
274 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::AbilityBuild);
275 if let Some(spell_ability_text) = game
276 .card(card_id)
277 .abilities
278 .iter()
279 .find(|a| crate::parsing::raw_has_key(a, keys::SP))
280 .cloned()
281 {
282 let host = game.card(card_id);
283 let mut sa = build_spell_ability_of_type(
284 host,
285 &spell_ability_text,
286 player,
287 AbilityRecordType::Spell,
288 );
289 if sa.pay_costs.is_none() {
291 sa.pay_costs = Some(Cost {
292 parts: vec![CostPart::Mana {
293 cost: host.mana_cost.clone(),
294 x_min: 0,
295 is_exiled_creature_cost: false,
296 is_enchanted_creature_cost: false,
297 is_cost_pay_any_number_of_times: false,
298 max_waterbend: None,
299 }],
300 has_tap: false,
301 mandatory: false,
302 });
303 }
304 if sa.target_restrictions.is_none() && host.type_line.has_subtype("Aura") {
308 let enchant_type = host.get_keyword_cost("Enchant").unwrap_or_default();
309 let params_str = crate::parsing::enchant_type_to_target_params(&enchant_type);
310 sa.target_restrictions = TargetRestrictions::new(&Params::from_raw(¶ms_str));
311 }
312 return sa;
313 }
314
315 let mut restriction = crate::spellability::SpellAbilityRestriction::default();
318 restriction.variables.set_zone(ZoneType::Hand);
319 let condition = crate::spellability::SpellAbilityCondition::default();
320 let card = game.card(card_id);
321
322 let target_restrictions = if card.type_line.has_subtype("Aura") {
326 let enchant_type = card.get_keyword_cost("Enchant").unwrap_or_default();
327 let params_str = crate::parsing::enchant_type_to_target_params(&enchant_type);
328 TargetRestrictions::new(&Params::from_raw(¶ms_str))
329 } else {
330 None
331 };
332
333 SpellAbility {
334 id: 0,
335 api: None,
336 source: Some(card_id),
337 original_host: card.effect_source,
338 activating_player: player,
339 targeting_player: None,
340 ability_text: String::new(),
341 record_type: AbilityRecordType::Spell,
342 ir: crate::ability::ability_ir::SpellAbilityIr::default(),
343 target_restrictions,
344 target_chosen: TargetChoices::default(),
345 pay_costs: Some(Cost {
346 parts: vec![CostPart::Mana {
347 cost: card.mana_cost.clone(),
348 x_min: 0,
349 is_exiled_creature_cost: false,
350 is_enchanted_creature_cost: false,
351 is_cost_pay_any_number_of_times: false,
352 max_waterbend: None,
353 }],
354 has_tap: false,
355 mandatory: false,
356 }),
357 sub_ability: None,
358 wrapped_ability: None,
359 is_spell: true,
360 is_trigger: false,
361 is_activated: false,
362 intrinsic: false,
363 trigger_source: None,
364 trigger_source_zone_timestamp: None,
365 source_zone_timestamp: Some(card.zone_timestamp),
366 source_trigger_id: None,
367 trigger_index: None,
368 alt_cost: None,
369 alt_cost_index: 0,
370 evoke_keyword_count: 0,
371 kicked: false,
372 buyback_paid: false,
373 overloaded: false,
374 is_copy: false,
375 paid_life_amount: 0,
376 kick_count: 0,
377 replicate_count: 0,
378 optional_generic_cost_paid: false,
379 trigger_remembered_amount: 0,
380 x_mana_cost_paid: 0,
381 discarded_cost_cards: Vec::new(),
382 optional_costs: Vec::new(),
383 paid_hash: std::collections::HashMap::new(),
384 paying_mana: Vec::new(),
385 paid_abilities: Vec::new(),
386 mana_part: None,
387 express_mana_choice: None,
388 convoke_tapped: Vec::new(),
389 spliced_cards: Vec::new(),
390 announce_vars: std::collections::HashMap::new(),
391 sacrificed_as_emerge: None,
392 sacrificed_as_offering: None,
393 description: String::new(),
394 stack_description: String::new(),
395 is_mana_ability: false,
396 is_land_ability: false,
397 cast_face_down: false,
398 trigger_objects: std::collections::HashMap::new(),
399 trigger_spell_abilities: std::collections::HashMap::new(),
400 additional_ability_lists: std::collections::HashMap::new(),
401 replacing_objects: std::collections::HashMap::new(),
402 trigger_remembered: Vec::new(),
403 restriction,
404 condition,
405 rollback_effects: Vec::new(),
406 optional_keyword_amounts: std::collections::HashMap::new(),
407 pips_to_reduce: Vec::new(),
408 may_choose_new_targets: false,
409 last_state: std::collections::HashMap::new(),
410 change_zone_table: None,
411 damage_map: None,
412 prevent_map: None,
413 }
414}
415
416fn build_spell_ability_of_type(
417 host: &Card,
418 ability_text: &str,
419 player: PlayerId,
420 record_type: AbilityRecordType,
421) -> SpellAbility {
422 let _perf_scope =
423 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::AbilityBuild);
424 crate::perf::increment_params_parse();
425 let parsed = ParsedParams::parse(ability_text);
426 let params = Params::from_parsed(&parsed);
427 build_spell_ability_of_type_with_params(
428 host,
429 ability_text,
430 player,
431 record_type,
432 &parsed,
433 params,
434 )
435}
436
437fn build_spell_ability_of_type_with_params(
438 host: &Card,
439 ability_text: &str,
440 player: PlayerId,
441 record_type: AbilityRecordType,
442 parsed: &ParsedParams<'_>,
443 params: Params,
444) -> SpellAbility {
445 let api = parsed
446 .get(record_type.prefix())
447 .and_then(ApiType::smart_value_of);
448 let mut ir = crate::ability::ability_ir::SpellAbilityIr::from_parsed(api, parsed);
449 ir.compile_numeric_params_from_runtime(¶ms);
450 let target_restrictions = if parsed.has(keys::VALID_TGTS) {
451 TargetRestrictions::new_from_parsed(parsed, ¶ms)
452 } else {
453 None
454 };
455 let cost = if record_type != AbilityRecordType::SubAbility {
456 parsed.get(keys::COST).map(parse_cost)
457 } else {
458 None
459 };
460 let mut restriction = crate::spellability::SpellAbilityRestriction::default();
461 let mut condition = crate::spellability::SpellAbilityCondition::default();
462
463 match record_type {
464 AbilityRecordType::Spell => restriction.variables.set_zone(ZoneType::Hand),
465 AbilityRecordType::Ability
466 | AbilityRecordType::StaticAbility
467 | AbilityRecordType::SubAbility => restriction.variables.set_zone(ZoneType::Battlefield),
468 }
469 if parsed.has_any(RESTRICTION_KEYS) {
470 restriction.set_restrictions_parsed(parsed);
471 }
472 if parsed.has_any(CONDITION_KEYS) {
473 condition.set_conditions_parsed(parsed);
474 }
475
476 let sub_ability = if let Some(sub_svar_name) = parsed.get(keys::SUB_ABILITY) {
478 let depth = SUB_ABILITY_CHAIN_DEPTH.with(|d| d.get());
479 if depth >= MAX_SUB_ABILITY_CHAIN_DEPTH {
480 eprintln!(
481 "SubAbility chain exceeded depth limit on {}, stopping at: {sub_svar_name}",
482 host.card_name
483 );
484 None
485 } else {
486 host.get_s_var(sub_svar_name)
487 .map(str::to_string)
488 .map(|sub_text| {
489 SUB_ABILITY_CHAIN_DEPTH.with(|d| d.set(depth + 1));
490 let sub = Box::new(build_spell_ability_from_host_card(host, &sub_text, player));
491 SUB_ABILITY_CHAIN_DEPTH.with(|d| d.set(depth));
492 sub
493 })
494 }
495 } else {
496 None
497 };
498
499 let mana_part = if parsed.has(keys::PRODUCED) {
500 build_mana_part_from_parsed(parsed)
501 } else {
502 None
503 };
504 let mut sa = SpellAbility {
505 id: 0,
506 api,
507 source: Some(host.id),
508 original_host: host.effect_source,
509 activating_player: player,
510 targeting_player: None,
511 ability_text: ability_text.to_string(),
512 record_type,
513 ir,
514 target_restrictions,
515 target_chosen: TargetChoices::default(),
516 pay_costs: cost,
517 sub_ability,
518 wrapped_ability: None,
519 is_spell: record_type == AbilityRecordType::Spell,
520 is_trigger: false,
521 is_activated: record_type == AbilityRecordType::Ability,
522 intrinsic: false,
523 trigger_source: None,
524 trigger_source_zone_timestamp: None,
525 source_zone_timestamp: Some(host.zone_timestamp),
526 source_trigger_id: None,
527 trigger_index: None,
528 alt_cost: None,
529 alt_cost_index: 0,
530 evoke_keyword_count: 0,
531 kicked: false,
532 buyback_paid: false,
533 overloaded: false,
534 is_copy: false,
535 paid_life_amount: 0,
536 kick_count: 0,
537 replicate_count: 0,
538 optional_generic_cost_paid: false,
539 trigger_remembered_amount: 0,
540 x_mana_cost_paid: 0,
541 discarded_cost_cards: Vec::new(),
542 optional_costs: Vec::new(),
543 paid_hash: std::collections::HashMap::new(),
544 paying_mana: Vec::new(),
545 paid_abilities: Vec::new(),
546 mana_part,
547 express_mana_choice: None,
548 convoke_tapped: Vec::new(),
549 spliced_cards: Vec::new(),
550 announce_vars: std::collections::HashMap::new(),
551 sacrificed_as_emerge: None,
552 sacrificed_as_offering: None,
553 description: String::new(),
554 stack_description: String::new(),
555 is_mana_ability: false,
556 is_land_ability: false,
557 cast_face_down: false,
558 trigger_objects: std::collections::HashMap::new(),
559 trigger_spell_abilities: std::collections::HashMap::new(),
560 additional_ability_lists: std::collections::HashMap::new(),
561 replacing_objects: std::collections::HashMap::new(),
562 trigger_remembered: Vec::new(),
563 restriction,
564 condition,
565 rollback_effects: Vec::new(),
566 optional_keyword_amounts: std::collections::HashMap::new(),
567 pips_to_reduce: Vec::new(),
568 may_choose_new_targets: false,
569 last_state: std::collections::HashMap::new(),
570 change_zone_table: None,
571 damage_map: None,
572 prevent_map: None,
573 };
574 if let Some(api) = api {
575 crate::ability::effects::build_spell_ability_for_api(api, &mut sa);
576 }
577 sa
578}
579
580#[allow(dead_code)]
581fn build_mana_part(params: &Params) -> Option<AbilityManaPart> {
582 let produced = params.get(keys::PRODUCED)?;
583 let mut mana_part = AbilityManaPart::new(produced, params.get(keys::RESTRICTION).unwrap_or(""));
584 mana_part.set_adds_keywords(params.get(keys::ADDS_KEYWORDS).map(str::to_string));
585 mana_part.set_triggers_when_spent(params.get(keys::TRIGGERS_WHEN_SPENT).map(str::to_string));
586 mana_part.set_persistent_mana(params.has("PersistentMana"));
587 mana_part.set_combat_mana(params.has("CombatMana"));
588 Some(mana_part)
589}
590
591fn build_mana_part_from_parsed(params: &ParsedParams<'_>) -> Option<AbilityManaPart> {
592 let produced = params.get(keys::PRODUCED)?;
593 let mut mana_part = AbilityManaPart::new(produced, params.get(keys::RESTRICTION).unwrap_or(""));
594 mana_part.set_adds_keywords(params.get(keys::ADDS_KEYWORDS).map(str::to_string));
595 mana_part.set_triggers_when_spent(params.get(keys::TRIGGERS_WHEN_SPENT).map(str::to_string));
596 mana_part.set_persistent_mana(params.has("PersistentMana"));
597 mana_part.set_combat_mana(params.has("CombatMana"));
598 Some(mana_part)
599}
600
601pub fn parse_ability_cost(
607 _host: &Card,
608 params: &Params,
609 record_type: AbilityRecordType,
610) -> Option<Cost> {
611 if record_type == AbilityRecordType::SubAbility {
612 return None;
613 }
614 params.get(keys::COST).map(parse_cost)
615}
616
617pub fn adjust_change_zone_target(sa: &mut SpellAbility, game: &GameState) {
624 if sa.ir.change_zone_table {
626 if sa.change_zone_table.is_none() {
630 sa.change_zone_table = Some(crate::card::card_zone_table::CardZoneTable::default());
631 }
632 }
633
634 if let Some(origin) = sa.ir.origin_zone {
637 let _ = game; if matches!(
639 origin,
640 forge_foundation::ZoneType::Library | forge_foundation::ZoneType::Hand
641 ) {
642 sa.ir.hidden = true;
644 }
645 }
646}
647
648pub fn build_fused_ability(
655 game: &GameState,
656 card_id: CardId,
657 player: PlayerId,
658) -> Option<SpellAbility> {
659 let card = game.card(card_id);
660
661 let has_fuse = card.keywords.contains_string_ignore_case("Fuse")
663 || card.granted_keywords.contains_string_ignore_case("Fuse");
664
665 if !has_fuse {
666 return None;
667 }
668
669 if card.abilities.len() < 2 {
671 return None;
672 }
673
674 let host = game.card(card_id);
676 let mut left_sa = build_spell_ability_from_host_card(host, &card.abilities[0], player);
677 left_sa.source = Some(card_id);
678
679 let right_sa = build_spell_ability_from_host_card(host, &card.abilities[1], player);
681
682 let mut slot = &mut left_sa.sub_ability;
684 loop {
685 match slot {
686 Some(node) => slot = &mut node.sub_ability,
687 None => {
688 *slot = Some(Box::new(right_sa));
689 break;
690 }
691 }
692 }
693
694 if let (Some(left_cost), Some(right_cost)) = (
696 &left_sa.pay_costs,
697 &card.abilities.get(1).and_then(|text| {
698 let params = Params::from_raw(text);
699 params.get(keys::COST).map(parse_cost)
700 }),
701 ) {
702 let mut combined_parts = left_cost.parts.clone();
703 combined_parts.extend(right_cost.parts.clone());
704 left_sa.pay_costs = Some(Cost {
705 parts: combined_parts,
706 has_tap: left_cost.has_tap || right_cost.has_tap,
707 mandatory: false,
708 });
709 }
710
711 left_sa.description = format!("Fuse (Cast both halves of {})", card.card_name);
712
713 Some(left_sa)
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn test_get_map_params() {
722 let input = "AB$ DealDamage | Cost$ T | NumDmg$ 1";
723 let map = get_map_params(input);
724 assert_eq!(map.get("AB").unwrap(), "DealDamage");
725 assert_eq!(map.get("Cost").unwrap(), "T");
726 assert_eq!(map.get("NumDmg").unwrap(), "1");
727 }
728
729 #[test]
730 fn test_record_type_from_params() {
731 let params = Params::from_raw("DB$ Draw | NumCards$ 2");
732 assert_eq!(
733 AbilityRecordType::from_params(¶ms),
734 Some(AbilityRecordType::SubAbility)
735 );
736 }
737
738 #[test]
739 fn test_record_type_from_params_static() {
740 let params = Params::from_raw("ST$ Continuous");
741 assert_eq!(
742 AbilityRecordType::from_params(¶ms),
743 Some(AbilityRecordType::StaticAbility)
744 );
745 }
746}