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}