manabrew_engine/ability/
activated.rs1use serde::{Deserialize, Serialize};
2
3use crate::ability::api_type::ApiType;
4use crate::ability::ProducedMana;
5use crate::cost::{parse_cost, Cost};
6use crate::parsing::keys;
7use crate::parsing::{parse_semantic_param_value, Params, SemanticParamValue};
8use forge_foundation::ZoneType;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ActivatedAbility {
14 pub ability_index: usize,
16 pub cost: Cost,
18 pub ability_text: String,
20 pub is_mana_ability: bool,
22 #[serde(default)]
24 pub ability_kind: String,
25 #[serde(default)]
27 pub ability_api: Option<ApiType>,
28 #[serde(default)]
30 pub activation_zone: Option<ZoneType>,
31 #[serde(default)]
33 pub produced_ir: Option<ProducedMana>,
34 #[serde(default)]
36 pub restrict_valid: Option<String>,
37 #[serde(default)]
39 pub amount: Option<String>,
40 #[serde(default)]
42 pub spell_description: Option<String>,
43 #[serde(default)]
45 pub precost_desc: Option<String>,
46 #[serde(default)]
48 pub description: Option<String>,
49 #[serde(default)]
51 pub adds_no_counter: bool,
52 #[serde(default)]
54 pub adds_keywords: Option<String>,
55 #[serde(default)]
57 pub adds_keywords_valid: Option<String>,
58 #[serde(default)]
60 pub adds_counters: Option<String>,
61 #[serde(default)]
63 pub adds_counters_valid: Option<String>,
64 #[serde(default)]
66 pub triggers_when_spent: Option<String>,
67 #[serde(default)]
69 pub sub_ability: Option<String>,
70 #[serde(default)]
72 pub spell_description_lower: String,
73 #[serde(default)]
75 pub game_activation_limit: Option<u32>,
76 #[serde(default)]
78 pub power_up: bool,
79 #[serde(default)]
81 pub sorcery_speed: bool,
82 #[serde(default)]
84 pub is_unlock_door: bool,
85 #[serde(default)]
87 pub is_mana_reflected: bool,
88 pub params: Params,
90}
91
92impl ActivatedAbility {
93 pub fn display_description(&self, card_name: &str) -> String {
94 self.spell_description
95 .as_deref()
96 .or(self.precost_desc.as_deref())
97 .or(self.description.as_deref())
98 .unwrap_or(&self.ability_text)
99 .replace("CARDNAME", card_name)
100 }
101
102 pub fn cost_string(&self) -> Option<String> {
103 let s = self.cost.to_simple_string();
104 (!s.is_empty()).then_some(s)
105 }
106
107 pub fn produced_color_symbols(&self) -> Vec<String> {
108 use crate::ability::{ProducedMana, ProducedManaCombo};
109 match &self.produced_ir {
110 Some(ProducedMana::Fixed(v))
111 | Some(ProducedMana::Combo(ProducedManaCombo::Colors(v))) => v.clone(),
112 _ => Vec::new(),
113 }
114 }
115}
116
117pub fn parse_activated_ability(raw: &str, index: usize) -> Option<ActivatedAbility> {
125 let params = Params::from_raw(raw);
126
127 let has_ab = params.has(keys::AB);
131 if !has_ab {
132 return None;
133 }
134
135 let cost_str = params.get(keys::COST).unwrap_or("");
137 let cost = parse_cost(cost_str);
138
139 let ab_type = params.get(keys::AB).unwrap_or("");
144 let has_targets = params.has(keys::VALID_TGTS);
145 let is_planeswalker_ability = params.is_true(keys::PLANESWALKER);
146 let is_mana_ability = (ab_type.eq_ignore_ascii_case("Mana")
147 || ab_type.eq_ignore_ascii_case("ManaReflected"))
148 && !has_targets
149 && !is_planeswalker_ability;
150 let activation_zone = params
151 .get(keys::ACTIVATION_ZONE)
152 .and_then(ZoneType::from_str_compat);
153 let game_activation_limit = params
154 .get(keys::GAME_ACTIVATION_LIMIT)
155 .and_then(|v| v.parse::<u32>().ok());
156 let power_up = params.is_true(keys::POWER_UP);
157 let sorcery_speed = params.is_true(keys::SORCERY_SPEED);
158 let is_unlock_door = ab_type.eq_ignore_ascii_case("UnlockDoor");
159 let is_mana_reflected = ab_type.eq_ignore_ascii_case("ManaReflected");
160 let ability_kind = ab_type.to_string();
161 let ability_api = ApiType::smart_value_of(ab_type);
162 let produced_ir = params.get(keys::PRODUCED).and_then(|raw| {
163 match parse_semantic_param_value(keys::PRODUCED, raw) {
164 SemanticParamValue::ProducedMana(produced) => {
165 Some(ProducedMana::from_semantic(&produced))
166 }
167 _ => None,
168 }
169 });
170 let restrict_valid = params.get(keys::RESTRICT_VALID).map(str::to_string);
171 let amount = params.get(keys::AMOUNT).map(str::to_string);
172 let spell_description = params.get(keys::SPELL_DESCRIPTION).map(str::to_string);
173 let precost_desc = params.get(keys::PRECOST_DESC).map(str::to_string);
174 let description = params.get(keys::DESCRIPTION).map(str::to_string);
175 let adds_no_counter = params.is_true(keys::ADDS_NO_COUNTER);
176 let adds_keywords = params.get(keys::ADDS_KEYWORDS).map(str::to_string);
177 let adds_keywords_valid = params.get(keys::ADDS_KEYWORDS_VALID).map(str::to_string);
178 let adds_counters = params.get(keys::ADDS_COUNTERS).map(str::to_string);
179 let adds_counters_valid = params.get(keys::ADDS_COUNTERS_VALID).map(str::to_string);
180 let triggers_when_spent = params.get(keys::TRIGGERS_WHEN_SPENT).map(str::to_string);
181 let sub_ability = params.get(keys::SUB_ABILITY).map(str::to_string);
182 let spell_description_lower = params
183 .get(keys::SPELL_DESCRIPTION)
184 .unwrap_or("")
185 .to_ascii_lowercase();
186
187 Some(ActivatedAbility {
188 ability_index: index,
189 cost,
190 ability_text: raw.to_string(),
191 is_mana_ability,
192 ability_kind,
193 ability_api,
194 activation_zone,
195 produced_ir,
196 restrict_valid,
197 amount,
198 spell_description,
199 precost_desc,
200 description,
201 adds_no_counter,
202 adds_keywords,
203 adds_keywords_valid,
204 adds_counters,
205 adds_counters_valid,
206 triggers_when_spent,
207 sub_ability,
208 spell_description_lower,
209 game_activation_limit,
210 power_up,
211 sorcery_speed,
212 is_unlock_door,
213 is_mana_reflected,
214 params,
215 })
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn parse_llanowar_elves_mana_ability() {
224 let raw = "AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}.";
225 let ab = parse_activated_ability(raw, 0).unwrap();
226 assert!(ab.is_mana_ability);
227 assert!(ab.cost.has_tap);
228 assert_eq!(
229 ab.produced_ir
230 .as_ref()
231 .map(ProducedMana::as_script_text)
232 .as_deref(),
233 Some("G")
234 );
235 }
236
237 #[test]
238 fn parse_planeswalker_mana_ability_uses_stack() {
239 let raw = "AB$ Mana | Cost$ AddCounter<0/LOYALTY> | Planeswalker$ True | Produced$ C | Amount$ 3 | SpellDescription$ Add {C}{C}{C}.";
240 let ab = parse_activated_ability(raw, 0).unwrap();
241 assert!(!ab.is_mana_ability);
242 }
243
244 #[test]
245 fn parse_prodigal_sorcerer_non_mana() {
246 let raw = "AB$ DealDamage | Cost$ T | ValidTgts$ Any | NumDmg$ 1 | SpellDescription$ CARDNAME deals 1 damage to any target.";
247 let ab = parse_activated_ability(raw, 0).unwrap();
248 let ActivatedAbility {
249 params: raw_params, ..
250 } = &ab;
251 assert!(!ab.is_mana_ability);
252 assert!(ab.cost.has_tap);
253 assert_eq!(raw_params.get(keys::NUM_DMG).unwrap(), "1");
254 }
255
256 #[test]
257 fn parse_sakura_tribe_elder_sacrifice() {
258 let raw = "AB$ ChangeZone | Cost$ Sac<1/CARDNAME> | Origin$ Library | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.Basic | SpellDescription$ Search for a basic land.";
259 let ab = parse_activated_ability(raw, 0).unwrap();
260 let ActivatedAbility {
261 params: raw_params, ..
262 } = &ab;
263 assert!(!ab.is_mana_ability);
264 assert!(!ab.cost.has_tap);
265 assert_eq!(raw_params.get(keys::ORIGIN).unwrap(), "Library");
266 assert_eq!(raw_params.get(keys::DESTINATION).unwrap(), "Battlefield");
267 }
268
269 #[test]
270 fn sp_line_returns_none() {
271 let raw = "SP$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SpellDescription$ test";
272 assert!(parse_activated_ability(raw, 0).is_none());
273 }
274
275 #[test]
276 fn db_line_returns_none() {
277 let raw = "DB$ Draw | Defined$ You | NumCards$ 2";
278 assert!(parse_activated_ability(raw, 0).is_none());
279 }
280}