manabrew_engine/combat/
attack_restriction.rs1use std::collections::HashSet;
2
3use forge_foundation::ZoneType;
4
5use crate::card::Card;
6use crate::ids::CardId;
7use crate::staticability::StaticMode;
8
9pub use super::attack_restriction_type::AttackRestrictionType;
10
11pub fn get_restrictions(card: &Card) -> HashSet<AttackRestrictionType> {
14 let mut restrictions = HashSet::new();
15
16 for kw in card
17 .keywords
18 .iter_strings()
19 .chain(card.granted_keywords.iter_strings())
20 .chain(card.pump_keywords.iter_strings())
21 {
22 let kw_lower = kw.to_lowercase();
25
26 if kw_lower.contains("can only attack alone") {
27 restrictions.insert(AttackRestrictionType::OnlyAlone);
28 }
29 if kw_lower.contains("can't attack alone")
30 || kw_lower.contains("can't attack or block alone")
31 {
32 restrictions.insert(AttackRestrictionType::NotAlone);
33 }
34 if kw_lower.contains("can't attack unless a creature with greater power also attacks") {
35 restrictions.insert(AttackRestrictionType::NeedGreaterPower);
36 }
37 if kw_lower.contains("can't attack unless a black or green creature also attacks") {
38 restrictions.insert(AttackRestrictionType::NeedBlackOrGreen);
39 }
40 if kw_lower.contains("can't attack unless at least two other creatures attack") {
41 restrictions.insert(AttackRestrictionType::NeedTwoOthers);
42 }
43 }
44
45 for st_ab in &card.static_abilities {
47 if st_ab.check_mode(&StaticMode::CantAttack) {
48 if let Some(restriction) = st_ab.ir.restriction_text.as_deref() {
49 match restriction {
50 "OnlyAlone" => {
51 restrictions.insert(AttackRestrictionType::OnlyAlone);
52 }
53 "NotAlone" => {
54 restrictions.insert(AttackRestrictionType::NotAlone);
55 }
56 "NeedGreaterPower" => {
57 restrictions.insert(AttackRestrictionType::NeedGreaterPower);
58 }
59 "NeedBlackOrGreen" => {
60 restrictions.insert(AttackRestrictionType::NeedBlackOrGreen);
61 }
62 "NeedTwoOthers" => {
63 restrictions.insert(AttackRestrictionType::NeedTwoOthers);
64 }
65 "Never" => {
66 restrictions.insert(AttackRestrictionType::Never);
67 }
68 _ => {}
69 }
70 }
71 }
72 }
73
74 if restrictions.contains(&AttackRestrictionType::OnlyAlone)
77 && (restrictions.contains(&AttackRestrictionType::NeedGreaterPower)
78 || restrictions.contains(&AttackRestrictionType::NeedBlackOrGreen)
79 || restrictions.contains(&AttackRestrictionType::NotAlone)
80 || restrictions.contains(&AttackRestrictionType::NeedTwoOthers))
81 {
82 restrictions.insert(AttackRestrictionType::Never);
83 }
84
85 restrictions
86}
87
88pub fn can_attack(card: &Card, num_attackers: usize) -> bool {
91 let restrictions = get_restrictions(card);
92
93 if restrictions.contains(&AttackRestrictionType::Never) {
94 return false;
95 }
96 if restrictions.contains(&AttackRestrictionType::OnlyAlone) && num_attackers > 1 {
97 return false;
98 }
99 if restrictions.contains(&AttackRestrictionType::NotAlone) && num_attackers <= 1 {
100 return false;
101 }
102 if restrictions.contains(&AttackRestrictionType::NeedTwoOthers) && num_attackers <= 2 {
103 return false;
104 }
105 true
106}
107
108pub fn validate_attack_restrictions(attackers: &[CardId], cards: &[Card]) -> HashSet<CardId> {
114 let mut illegal = HashSet::new();
115 let num_attackers = attackers.len();
116
117 for &attacker_id in attackers {
118 let card = &cards[attacker_id.index()];
119 if card.zone != ZoneType::Battlefield {
120 illegal.insert(attacker_id);
121 continue;
122 }
123 let restrictions = get_restrictions(card);
124
125 if restrictions.contains(&AttackRestrictionType::Never) {
127 illegal.insert(attacker_id);
128 continue;
129 }
130
131 if restrictions.contains(&AttackRestrictionType::OnlyAlone) && num_attackers > 1 {
133 illegal.insert(attacker_id);
134 continue;
135 }
136
137 if restrictions.contains(&AttackRestrictionType::NotAlone) && num_attackers <= 1 {
139 illegal.insert(attacker_id);
140 continue;
141 }
142
143 if restrictions.contains(&AttackRestrictionType::NeedTwoOthers)
145 && (num_attackers as i32 - 1) < 2
146 {
147 illegal.insert(attacker_id);
148 continue;
149 }
150
151 if restrictions.contains(&AttackRestrictionType::NeedGreaterPower) {
154 let my_power = card.power();
155 let has_greater = attackers.iter().any(|&other_id| {
156 other_id != attacker_id && cards[other_id.index()].power() > my_power
157 });
158 if !has_greater {
159 illegal.insert(attacker_id);
160 continue;
161 }
162 }
163
164 if restrictions.contains(&AttackRestrictionType::NeedBlackOrGreen) {
166 let has_bg = attackers.iter().any(|&other_id| {
167 if other_id == attacker_id {
168 return false;
169 }
170 let other = &cards[other_id.index()];
171 other.color.has_black() || other.color.has_green()
172 });
173 if !has_bg {
174 illegal.insert(attacker_id);
175 continue;
176 }
177 }
178 }
179
180 let remaining: Vec<CardId> = attackers
182 .iter()
183 .copied()
184 .filter(|id| !illegal.contains(id))
185 .collect();
186 let remaining_count = remaining.len();
187
188 for &attacker_id in &remaining {
189 let card = &cards[attacker_id.index()];
190 let restrictions = get_restrictions(card);
191
192 if restrictions.contains(&AttackRestrictionType::NotAlone) && remaining_count <= 1 {
193 illegal.insert(attacker_id);
194 }
195 if restrictions.contains(&AttackRestrictionType::NeedTwoOthers)
196 && (remaining_count as i32 - 1) < 2
197 {
198 illegal.insert(attacker_id);
199 }
200 }
201
202 illegal
203}