Skip to main content

manabrew_engine/spellability/
spell_ability_restriction.rs

1//! Activation restrictions for spell abilities.
2//!
3//! Mirrors Java's `SpellAbilityRestriction.java` — determines whether a
4//! spell ability can be legally activated given the current game state.
5
6use 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/// Activation restrictions for a spell ability.
18/// Mirrors Java's `SpellAbilityRestriction` — wraps `SpellAbilityVariables`
19/// and checks game state conditions to determine if activation is legal.
20#[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    /// Create a new restriction with default variables.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Parse activation restrictions from ability params.
40    /// Mirrors Java's `SpellAbilityRestriction.setRestrictions(SpellAbility)`.
41    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        // Parse activation zone
69        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        // Parse phase restrictions
76        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        // Parse sorcery speed restriction
85        if is_true("SorcerySpeed") {
86            self.variables.set_sorcery_speed(true);
87        }
88
89        // Parse instant speed
90        if is_true("InstantSpeed") {
91            self.variables.set_instant_speed(true);
92        }
93
94        // Parse activator
95        if let Some(activator) = get("Activator") {
96            self.variables.set_activator(activator.to_string());
97        }
98
99        // Parse turn restrictions
100        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        // Parse activation limits
108        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        // Parse condition flags
117        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        // Parse presence check
143        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        // Parse cards in hand requirement
170        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    /// Check if this spell ability can be played given the current game state.
178    /// Mirrors Java's `SpellAbilityRestriction.canPlay(Card, SpellAbility)`.
179    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        // Check zone restriction
191        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        // Check phase restriction
201        let phases = self.variables.phases();
202        if !phases.is_empty() && !phases.contains(&game.turn.phase) {
203            return false;
204        }
205
206        // Check sorcery speed: must be a main phase and the stack must be empty
207        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        // Check turn restrictions
217        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        // Check cards in hand requirement
226        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    /// Check zone restrictions only.
324    /// Mirrors Java's `SpellAbilityRestriction.checkZoneRestrictions(Card, SpellAbility)`.
325    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    /// Check timing restrictions (phase, sorcery speed, etc.).
330    /// Mirrors Java's `SpellAbilityRestriction.checkTimingRestrictions(Card, SpellAbility)`.
331    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    /// Check activator restrictions.
355    /// Mirrors Java's `SpellAbilityRestriction.checkActivatorRestrictions(Card, SpellAbility)`.
356    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    /// Whether `player` may activate an ability hosted by `card_id`.
371    /// Mirrors the common Java valid-player cases used by `Activator$`.
372    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    /// Check other restrictions (threshold, metalcraft, etc.).
393    /// Mirrors Java's `SpellAbilityRestriction.checkOtherRestrictions(Card, SpellAbility)`.
394    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
420/// Parse a zone string into a ZoneType.
421fn 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}