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