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}