Skip to main content

manabrew_engine/spellability/
spell.rs

1//! Spell -- helper functions for cast spells.
2//! Mirrors Java's `Spell.java` (subclass of SpellAbility for spells cast from hand/zone).
3//! In Rust the subclass is flattened: SpellAbility.is_spell == true marks a spell.
4
5use forge_foundation::ZoneType;
6
7use crate::game::GameState;
8use crate::ids::CardId;
9use crate::spellability::SpellAbility;
10
11/// Type alias for SpellAbility when used as a cast spell.
12/// In Java, `Spell` is a subclass; in Rust it's the same struct with `is_spell = true`.
13pub type Spell = super::SpellAbility;
14
15/// Whether this spell can currently be played.
16/// Mirrors Java's `Spell.canPlay()`.
17///
18/// Checks:
19/// 1. The source card is not already on the battlefield.
20/// 2. No split-second spell is on the stack (unless this is a mana ability).
21/// 3. General restrictions pass.
22pub fn can_play(sa: &SpellAbility, game: &GameState) -> bool {
23    // Card must exist
24    let card_id = match sa.source {
25        Some(id) => id,
26        None => return false,
27    };
28
29    // A spell cannot be cast if its card is already on the battlefield
30    if game.card_is_in_zone(card_id, ZoneType::Battlefield) {
31        return false;
32    }
33
34    // Split second check: if any spell on the stack has split second,
35    // only mana abilities may be activated
36    if super::has_split_second_on_stack(game) && !sa.is_mana_ability {
37        return false;
38    }
39
40    if !sa.can_cast_timing(game) {
41        return false;
42    }
43
44    // Delegate to general restriction check
45    check_restrictions(sa, game)
46}
47
48/// Check cant-be-cast restrictions from static abilities.
49/// Mirrors Java's `Spell.checkRestrictions()`.
50///
51/// Walks static abilities on the battlefield looking for "can't cast" effects
52/// that apply to this spell. Returns true if casting is allowed.
53pub fn check_restrictions(sa: &SpellAbility, game: &GameState) -> bool {
54    // Base restriction check from SpellAbility itself
55    sa.can_play(game)
56}
57
58/// Whether this spell can be played from its host card.
59/// Mirrors Java's `Spell.canPlayFromHost()`.
60/// Returns the card if playable, None otherwise.
61pub fn can_play_from_host(sa: &SpellAbility, game: &GameState) -> Option<CardId> {
62    let card_id = sa.source?;
63    // A spell cannot be cast if its card is already on the battlefield
64    if game.card_is_in_zone(card_id, ZoneType::Battlefield) {
65        return None;
66    }
67
68    // Split second check
69    if super::has_split_second_on_stack(game) && !sa.is_mana_ability {
70        return None;
71    }
72
73    if !sa.can_cast_timing(game) {
74        return None;
75    }
76
77    // Restrictions check
78    if !sa.can_play(game) {
79        return None;
80    }
81
82    Some(card_id)
83}
84
85/// Whether this spell can be countered by the given counter spell/ability.
86/// Mirrors Java's `Spell.isCounterableBy(SpellAbility)`.
87///
88/// A spell is counterable unless it has "can't be countered" (e.g. Abrupt Decay).
89pub fn is_counterable_by(sa: &SpellAbility, _counter_sa: &SpellAbility) -> bool {
90    // Check if the spell has the "CantBeCountered" flag in params
91    if sa.ir.cant_be_countered {
92        return false;
93    }
94
95    // Check if the source card has "This spell can't be countered" keyword
96    // This is handled by checking the param rather than the card keyword
97    // because by the time it's on the stack, the SA params are authoritative.
98    true
99}
100
101/// Set whether this spell is cast face down (Morph).
102/// Mirrors Java's `Spell.setCastFaceDown(boolean)`.
103pub fn set_cast_face_down(sa: &mut SpellAbility, face_down: bool) {
104    sa.cast_face_down = face_down;
105}