manabrew_engine/spellability/
spell_ability_restriction.rs1use forge_foundation::{PhaseType, ZoneType};
7use serde::{Deserialize, Serialize};
8
9use crate::game::GameState;
10use crate::ids::{CardId, PlayerId};
11use crate::parsing::compare::compare_expr;
12use crate::parsing::{Params, ParsedParams};
13use crate::spellability::SpellAbility;
14
15use super::spell_ability_variables::SpellAbilityVariables;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SpellAbilityRestriction {
22 pub variables: SpellAbilityVariables,
23}
24
25impl Default for SpellAbilityRestriction {
26 fn default() -> Self {
27 Self {
28 variables: SpellAbilityVariables::new(),
29 }
30 }
31}
32
33impl SpellAbilityRestriction {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn set_restrictions(&mut self, params: &Params) {
42 self.set_restrictions_from(|key| params.get(key));
43 }
44
45 pub fn set_restrictions_parsed(&mut self, params: &ParsedParams<'_>) {
46 self.set_restrictions_from(|key| params.get(key));
47 }
48
49 fn set_restrictions_from<'a, F>(&mut self, get: F)
50 where
51 F: Fn(&str) -> Option<&'a str>,
52 {
53 let is_true = |key| get(key).is_some_and(|value| value.eq_ignore_ascii_case("True"));
54
55 if let Some(value) = get("Activation") {
56 match value {
57 "Threshold" => self.variables.set_threshold(true),
58 "Metalcraft" => self.variables.set_metalcraft(true),
59 "Delirium" => self.variables.set_delirium(true),
60 "Hellbent" => self.variables.set_hellbent(true),
61 "Desert" => self.variables.set_desert(true),
62 "Blessing" => self.variables.set_blessing(true),
63 "Solved" => self.variables.set_solved(true),
64 _ => {}
65 }
66 }
67
68 if let Some(zone_str) = get("ActivationZone") {
70 if let Some(zone) = parse_zone(zone_str) {
71 self.variables.set_zone(zone);
72 }
73 }
74
75 if let Some(phases_str) = get("ActivationPhases") {
77 for phase_name in phases_str.split(',') {
78 if let Some(phase) = PhaseType::from_script_name(phase_name.trim()) {
79 self.variables.add_phase(phase);
80 }
81 }
82 }
83
84 if is_true("SorcerySpeed") {
86 self.variables.set_sorcery_speed(true);
87 }
88
89 if is_true("InstantSpeed") {
91 self.variables.set_instant_speed(true);
92 }
93
94 if let Some(activator) = get("Activator") {
96 self.variables.set_activator(activator.to_string());
97 }
98
99 if is_true("PlayerTurn") {
101 self.variables.set_player_turn(true);
102 }
103 if is_true("OpponentTurn") {
104 self.variables.set_opponent_turn(true);
105 }
106
107 if let Some(limit) = get("ActivationLimit") {
109 self.variables.set_limit_to_check(Some(limit.to_string()));
110 }
111 if let Some(game_limit) = get("GameActivationLimit") {
112 self.variables
113 .set_game_limit_to_check(Some(game_limit.to_string()));
114 }
115
116 if is_true("Threshold") {
118 self.variables.set_threshold(true);
119 }
120 if is_true("Metalcraft") {
121 self.variables.set_metalcraft(true);
122 }
123 if is_true("Delirium") {
124 self.variables.set_delirium(true);
125 }
126 if is_true("Hellbent") {
127 self.variables.set_hellbent(true);
128 }
129 if is_true("Revolt") {
130 self.variables.set_revolt(true);
131 }
132 if is_true("Desert") {
133 self.variables.set_desert(true);
134 }
135 if is_true("Blessing") {
136 self.variables.set_blessing(true);
137 }
138 if is_true("Solved") {
139 self.variables.set_solved(true);
140 }
141
142 if let Some(present) = get("IsPresent") {
144 self.variables.set_is_present(Some(present.to_string()));
145 }
146 if let Some(compare) = get("PresentCompare") {
147 self.variables
148 .set_present_compare(Some(compare.to_string()));
149 }
150 if let Some(zone_str) = get("PresentZone") {
151 if let Some(zone) = parse_zone(zone_str) {
152 self.variables.set_present_zone(zone);
153 }
154 }
155 if let Some(defined) = get("PresentDefined") {
156 self.variables
157 .set_present_defined(Some(defined.to_string()));
158 }
159
160 if let Some(class_level) = get("ClassLevel") {
161 if class_level.len() >= 2 {
162 self.variables
163 .set_class_level_operator(Some(class_level[..2].to_string()));
164 self.variables
165 .set_class_level(Some(class_level[2..].to_string()));
166 }
167 }
168
169 if let Some(count_str) = get("ActivateCardsInHand") {
171 if let Ok(count) = count_str.parse::<i32>() {
172 self.variables.set_cards_in_hand(count);
173 }
174 }
175 }
176
177 pub fn can_play(&self, game: &GameState, card_id: CardId, player: PlayerId) -> bool {
180 self.can_play_with_sa(game, card_id, player, None)
181 }
182
183 pub fn can_play_with_sa(
184 &self,
185 game: &GameState,
186 card_id: CardId,
187 player: PlayerId,
188 sa: Option<&SpellAbility>,
189 ) -> bool {
190 let card_zone = game.card_current_zone(card_id);
192 if card_zone != self.variables.zone() {
193 return false;
194 }
195 let card = game.card(card_id);
196 if !self.can_player_activate_host(game, card_id, player) {
197 return false;
198 }
199
200 let phases = self.variables.phases();
202 if !phases.is_empty() && !phases.contains(&game.turn.phase) {
203 return false;
204 }
205
206 if self.variables.sorcery_speed() {
208 let is_main = game.turn.phase.is_main();
209 let stack_empty = game.stack.is_empty();
210 let is_active = game.turn.active_player == player;
211 if !is_main || !stack_empty || !is_active {
212 return false;
213 }
214 }
215
216 let is_players_turn = game.turn.active_player == player;
218 if self.variables.player_turn() && !is_players_turn {
219 return false;
220 }
221 if self.variables.opponent_turn() && is_players_turn {
222 return false;
223 }
224
225 let required = self.variables.cards_in_hand();
227 if required >= 0 && game.player_hand_count(player) < required as usize {
228 return false;
229 }
230
231 if self.variables.hellbent() && !game.player_has_hellbent(player) {
232 return false;
233 }
234
235 if self.variables.threshold() && !game.player_has_threshold(player) {
236 return false;
237 }
238
239 if self.variables.metalcraft() && !game.player_has_metalcraft(player) {
240 return false;
241 }
242
243 if self.variables.delirium() && !game.player_has_delirium(player) {
244 return false;
245 }
246
247 if self.variables.revolt() && !game.player_has_revolt(player) {
248 return false;
249 }
250
251 if self.variables.desert() && !game.player_has_desert(player) {
252 return false;
253 }
254
255 if self.variables.blessing() && !game.player_has_blessing(player) {
256 return false;
257 }
258
259 if !self.check_presence_restriction(game, card_id, player, sa) {
260 return false;
261 }
262
263 if let Some(class_level) = self.variables.class_level() {
264 let Some(operator) = self.variables.class_level_operator() else {
265 return false;
266 };
267 let operand = class_level.parse::<i32>().unwrap_or(0);
268 if !compare_expr(card.class_level, &format!("{operator}{operand}")) {
269 return false;
270 }
271 }
272
273 true
274 }
275
276 fn check_presence_restriction(
277 &self,
278 game: &GameState,
279 card_id: CardId,
280 player: PlayerId,
281 sa: Option<&SpellAbility>,
282 ) -> bool {
283 let Some(is_present) = self.variables.is_present() else {
284 return true;
285 };
286 let cards = if let Some(defined) = self.variables.present_defined() {
287 crate::ability::ability_utils::get_defined_cards(
288 game,
289 Some(card_id),
290 defined,
291 Some(player),
292 )
293 } else {
294 game.cards_in_zone(self.variables.present_zone(), player)
295 .to_vec()
296 };
297 let count = cards
298 .into_iter()
299 .filter(|&cid| {
300 if let Some(sa) = sa {
301 crate::ability::ability_utils::matches_valid_cards_for_sa(
302 game,
303 sa,
304 game.card(cid),
305 None,
306 is_present,
307 )
308 } else {
309 crate::ability::ability_utils::matches_valid_cards_for_source(
310 game,
311 card_id,
312 game.card(cid),
313 None,
314 is_present,
315 )
316 }
317 })
318 .count() as i32;
319 let compare = self.variables.present_compare().unwrap_or("GE1");
320 compare_expr(count, compare)
321 }
322
323 pub fn check_zone_restrictions(&self, game: &GameState, card_id: CardId) -> bool {
326 game.card_current_zone(card_id) == self.variables.zone()
327 }
328
329 pub fn check_timing_restrictions(&self, game: &GameState, player: PlayerId) -> bool {
332 let phases = self.variables.phases();
333 if !phases.is_empty() && !phases.contains(&game.turn.phase) {
334 return false;
335 }
336 if self.variables.sorcery_speed() {
337 let is_main = game.turn.phase.is_main();
338 let stack_empty = game.stack.is_empty();
339 let is_active = game.turn.active_player == player;
340 if !is_main || !stack_empty || !is_active {
341 return false;
342 }
343 }
344 let is_players_turn = game.turn.active_player == player;
345 if self.variables.player_turn() && !is_players_turn {
346 return false;
347 }
348 if self.variables.opponent_turn() && is_players_turn {
349 return false;
350 }
351 true
352 }
353
354 pub fn check_activator_restrictions(&self, game: &GameState, player: PlayerId) -> bool {
357 let activator = self.variables.activator();
358 if activator == "Player" {
359 return true;
360 }
361 if activator == "You" {
362 return true;
363 }
364 if activator == "Opponent" {
365 return game.turn.active_player != player;
366 }
367 true
368 }
369
370 pub fn can_player_activate_host(
373 &self,
374 game: &GameState,
375 card_id: CardId,
376 player: PlayerId,
377 ) -> bool {
378 let controller = game.card(card_id).controller;
379 match self.variables.activator() {
380 "Player" => true,
381 "You" => player == controller,
382 "Opponent" => player != controller,
383 activator if activator.starts_with("Player.PlayerUID_") => activator
384 .strip_prefix("Player.PlayerUID_")
385 .and_then(|id| id.parse::<u32>().ok())
386 .map(|id| player.0 == id)
387 .unwrap_or(false),
388 _ => player == controller,
389 }
390 }
391
392 pub fn check_other_restrictions(&self, game: &GameState, player: PlayerId) -> bool {
395 if self.variables.hellbent() && !game.player_has_hellbent(player) {
396 return false;
397 }
398 if self.variables.threshold() && !game.player_has_threshold(player) {
399 return false;
400 }
401 if self.variables.metalcraft() && !game.player_has_metalcraft(player) {
402 return false;
403 }
404 if self.variables.delirium() && !game.player_has_delirium(player) {
405 return false;
406 }
407 if self.variables.revolt() && !game.player_has_revolt(player) {
408 return false;
409 }
410 if self.variables.desert() && !game.player_has_desert(player) {
411 return false;
412 }
413 if self.variables.blessing() && !game.player_has_blessing(player) {
414 return false;
415 }
416 true
417 }
418}
419
420fn parse_zone(s: &str) -> Option<ZoneType> {
422 match s.to_lowercase().as_str() {
423 "battlefield" => Some(ZoneType::Battlefield),
424 "hand" => Some(ZoneType::Hand),
425 "graveyard" => Some(ZoneType::Graveyard),
426 "library" => Some(ZoneType::Library),
427 "exile" => Some(ZoneType::Exile),
428 "command" => Some(ZoneType::Command),
429 _ => None,
430 }
431}