1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Alternative cost getters for Card.
//!
//! These methods extract cost information from keywords following the "Keyword:cost" pattern.
//! They're used by the casting/playability system to present alternative casting options.
use super::Card;
impl Card {
// ── Keyword cost helpers (pattern: "Keyword:cost_string") ────────
/// Get buyback cost (e.g. "Buyback:2" → Some("2")).
pub fn get_buyback_cost(&self) -> Option<String> {
self.get_keyword_cost("Buyback")
}
/// Get spectacle cost (e.g. "Spectacle:B R" → Some("B R")).
pub fn get_spectacle_cost(&self) -> Option<String> {
self.get_keyword_cost("Spectacle")
}
/// Get evoke cost (e.g. "Evoke:2 B" → Some("2 B")).
pub fn get_evoke_cost(&self) -> Option<String> {
self.get_keyword_cost("Evoke")
}
/// Get every Evoke cost on this card (intrinsic + granted). Multiple Evoke
/// keywords stack independently — e.g. an Elemental in hand under Ashling,
/// the Limitless has both its native `Evoke:2 U` and the granted `Evoke:4`.
/// Each is a distinct alternative cost in MTG and a separate playable entry.
pub fn get_all_evoke_costs(&self) -> Vec<String> {
let prefix = "Evoke:";
let mut out = Vec::new();
for kw in self.keywords.iter_strings() {
if let Some(cost) = kw.strip_prefix(prefix) {
out.push(cost.to_string());
}
}
for kw in self.granted_keywords.iter_strings() {
if let Some(cost) = kw.strip_prefix(prefix) {
out.push(cost.to_string());
}
}
out
}
/// Get bestow cost (e.g. "Bestow:3 G G" → Some("3 G G")).
pub fn get_bestow_cost(&self) -> Option<String> {
self.get_keyword_cost("Bestow")
}
/// Get dash cost (e.g. "Dash:1 R" → Some("1 R")).
pub fn get_dash_cost(&self) -> Option<String> {
self.get_keyword_cost("Dash")
}
/// Get blitz cost (e.g. "Blitz:1 R" → Some("1 R")).
pub fn get_blitz_cost(&self) -> Option<String> {
self.get_keyword_cost("Blitz")
}
/// Get warp cost (e.g. "Warp:1 B" → Some("1 B")).
pub fn get_warp_cost(&self) -> Option<String> {
self.get_keyword_cost("Warp")
}
/// Get multikicker cost (e.g. "Multikicker:1 G" → Some("1 G")).
pub fn get_multikicker_cost(&self) -> Option<String> {
self.get_keyword_cost("Multikicker")
}
/// Get replicate cost (e.g. "Replicate:U" → Some("U")).
pub fn get_replicate_cost(&self) -> Option<String> {
self.get_keyword_cost("Replicate")
}
/// Get entwine cost (e.g. "Entwine:2" → Some("2")).
pub fn get_entwine_cost(&self) -> Option<String> {
self.get_keyword_cost("Entwine")
}
/// Get escalate cost (e.g. "Escalate:1" → Some("1")).
pub fn get_escalate_cost(&self) -> Option<String> {
self.get_keyword_cost("Escalate")
}
/// Get escape cost and exile count (e.g. "Escape:1 B B:4" → Some(("1 B B", 4))).
/// Delegates parsing to the keyword module.
pub fn get_escape_cost(&self) -> Option<(String, i32)> {
crate::keyword::extract_escape(&self.keywords)
.or_else(|| crate::keyword::extract_escape(&self.granted_keywords))
.map(|info| (info.mana_cost, info.exile_count))
}
/// Get overload cost (e.g. "Overload:3 R" → Some("3 R")).
pub fn get_overload_cost(&self) -> Option<String> {
self.get_keyword_cost("Overload")
}
/// Get madness cost (e.g. "Madness:1 R" → Some("1 R")).
pub fn get_madness_cost(&self) -> Option<String> {
self.get_keyword_cost("Madness")
}
/// Get strive cost (e.g. "Strive:1 W" → Some("1 W")).
pub fn get_strive_cost(&self) -> Option<String> {
self.get_keyword_cost("Strive")
}
/// Get suspend cost and time counters (e.g. "Suspend:1 U:3" → Some(("1 U", 3))).
/// Delegates parsing to the keyword module.
pub fn get_suspend_cost(&self) -> Option<(String, i32)> {
crate::keyword::extract_suspend(&self.keywords)
.or_else(|| crate::keyword::extract_suspend(&self.granted_keywords))
.map(|info| (info.mana_cost, info.time_counters))
}
/// Get foretell cost (e.g. "Foretell:W W" → Some("W W")).
pub fn get_foretell_cost(&self) -> Option<String> {
self.get_keyword_cost("Foretell")
}
/// Get emerge cost (e.g. "Emerge:5 U U" → Some("5 U U")).
pub fn get_emerge_cost(&self) -> Option<String> {
self.get_keyword_cost("Emerge")
}
/// Get offering type (e.g. "Offering:Snake" → Some("Snake")).
pub fn get_offering_type(&self) -> Option<String> {
self.get_keyword_cost("Offering")
}
/// Generic keyword cost parser — delegates to the keyword module.
/// Looks for "Keyword:cost" in intrinsic and granted keywords.
pub fn get_keyword_cost(&self, keyword: &str) -> Option<String> {
crate::keyword::extract_keyword_cost_from_all(
[&self.keywords, &self.granted_keywords],
keyword,
)
}
/// Get a keyword's numeric amount (e.g. "Dredge:2" → Some(2)).
pub fn get_keyword_amount(&self, keyword: &str) -> Option<usize> {
self.get_keyword_cost(keyword)
.and_then(|s| s.trim().parse::<usize>().ok())
}
/// Get Ward cost (e.g. "Ward:2" → Some("2"), "Ward:{U}" → Some("{U}")).
pub fn get_ward_cost(&self) -> Option<String> {
self.get_keyword_cost("Ward")
}
pub fn get_flashback_cost(&self) -> Option<String> {
if let Some(cost) = self.get_keyword_cost("Flashback") {
return Some(cost);
}
if self.has_keyword("Flashback") && !self.mana_cost.is_no_cost() {
return Some(mana_cost_script_string(&self.mana_cost));
}
None
}
/// Get Harmonize cost (e.g. "Harmonize:X G G" → Some("X G G")).
pub fn get_harmonize_cost(&self) -> Option<String> {
self.get_keyword_cost("Harmonize")
}
/// Get Kicker cost (e.g. "Kicker:W" → Some("W")).
pub fn get_kicker_cost(&self) -> Option<String> {
self.get_keyword_cost("Kicker")
}
}
fn mana_cost_script_string(mc: &forge_foundation::ManaCost) -> String {
let mut tokens: Vec<String> = Vec::new();
for shard in mc.shards() {
if *shard == forge_foundation::ManaCostShard::X {
tokens.push("X".to_string());
}
}
if mc.generic_cost() > 0 {
tokens.push(mc.generic_cost().to_string());
}
for shard in mc.shards() {
if *shard != forge_foundation::ManaCostShard::X {
tokens.push(shard.short_string().to_string());
}
}
tokens.join(" ")
}