Skip to main content

manabrew_engine/spellability/
ability_activated.rs

1//! AbilityActivated -- helper functions for activated abilities.
2//! Mirrors Java's `AbilityActivated.java`.
3//! In Rust the subclass is flattened: SpellAbility.is_activated == true.
4
5use forge_foundation::ZoneType;
6
7use crate::game::GameState;
8use crate::spellability::SpellAbility;
9
10/// Whether this activated ability can currently be played.
11/// Mirrors Java's `AbilityActivated.canPlay()`.
12///
13/// Checks:
14/// 1. Cost is not marked unpayable.
15/// 2. Split second not on stack (unless mana ability).
16/// 3. Not suppressed (e.g. Pithing Needle naming this card).
17/// 4. Not detained.
18/// 5. General restrictions pass.
19pub fn can_play(sa: &SpellAbility, game: &GameState) -> bool {
20    // Unpayable cost check
21    if sa.ir.unpayable {
22        return false;
23    }
24
25    // Split second check: mana abilities bypass
26    if !sa.is_mana_ability && super::has_split_second_on_stack(game) {
27        return false;
28    }
29
30    // Check if the source card is suppressed (Pithing Needle, etc.)
31    if is_suppressed(sa, game) {
32        return false;
33    }
34
35    // Check if the source card is detained
36    if is_detained(sa, game) {
37        return false;
38    }
39
40    // Delegate to restriction check
41    check_restrictions(sa, game)
42}
43
44/// Whether this activated ability is possible (zone and activator checks).
45/// Mirrors Java's `AbilityActivated.isPossible()`.
46///
47/// A lighter check than `can_play` -- used to filter the list of
48/// abilities shown to the player before doing full legality checks.
49pub fn is_possible_activated(sa: &SpellAbility, game: &GameState) -> bool {
50    let card_id = match sa.source {
51        Some(id) => id,
52        None => return false,
53    };
54
55    // Check activation zone restriction -- activated abilities default to battlefield
56    if !game.card_is_in_zone(card_id, ZoneType::Battlefield) {
57        return false;
58    }
59
60    // The activating player must be alive
61    let player = game.player(sa.activating_player);
62    if !player.is_alive() {
63        return false;
64    }
65
66    true
67}
68
69/// Check cant-be-activated restrictions from static abilities.
70/// Mirrors Java's `AbilityActivated.checkRestrictions()`.
71pub fn check_restrictions(sa: &SpellAbility, game: &GameState) -> bool {
72    sa.can_play(game)
73}
74
75/// Whether to prompt even if this is the only possible ability.
76/// Mirrors Java's `AbilityActivated.promptIfOnlyPossibleAbility()`.
77/// Returns false -- activated abilities don't need confirmation when they're
78/// the only option (unlike spells which may have alternative costs).
79pub fn prompt_if_only_possible_ability() -> bool {
80    false
81}
82
83/// Check if the source card is suppressed by a naming effect (Pithing Needle).
84/// A card is suppressed if an opponent controls a permanent that names it
85/// and prevents activated abilities.
86pub fn is_suppressed(sa: &SpellAbility, game: &GameState) -> bool {
87    let card_id = match sa.source {
88        Some(id) => id,
89        None => return false,
90    };
91
92    let card = game.card(card_id);
93    let card_name = &card.card_name;
94
95    // Walk all players' battlefields looking for suppression effects
96    // (e.g. Pithing Needle naming this card via svars)
97    for &pid in &game.player_order {
98        if pid == sa.activating_player {
99            continue;
100        }
101        for &cid in game.cards_in_zone(ZoneType::Battlefield, pid) {
102            let perm = game.card(cid);
103            if let Some(named) = perm.get_s_var("ChosenName") {
104                if named.eq_ignore_ascii_case(card_name)
105                    && perm.has_keyword("Suppress activated abilities")
106                {
107                    return true;
108                }
109            }
110        }
111    }
112    false
113}
114
115/// Check if the source card is detained (can't attack, block, or activate abilities).
116pub fn is_detained(sa: &SpellAbility, game: &GameState) -> bool {
117    let card_id = match sa.source {
118        Some(id) => id,
119        None => return false,
120    };
121    let card = game.card(card_id);
122    card.has_keyword("HIDDEN Detained")
123}