1use std::collections::BTreeMap;
2
3use crate::agent::{GameEntity, PlayerAgent};
4use crate::card::CounterType;
5use crate::event::{CounterTableEntry, RunParams};
6use crate::game::GameState;
7use crate::ids::PlayerId;
8use crate::replacement::replacement_handler::{
9 apply_replacements, apply_replacements_with_agents, CounterMapValue, ReplacementEvent,
10};
11use crate::replacement::ReplacementResult;
12use crate::spellability::SpellAbility;
13use crate::trigger::{TriggerHandler, TriggerType};
14
15#[derive(Debug, Clone)]
16struct CounterCell {
17 source: Option<PlayerId>,
18 object: GameEntity,
19 counters: BTreeMap<CounterType, i32>,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct GameEntityCounterTable {
24 cells: Vec<CounterCell>,
25}
26
27impl GameEntityCounterTable {
28 pub fn from_counter_map(
29 object: GameEntity,
30 counter_map: Vec<CounterMapValue>,
31 ) -> GameEntityCounterTable {
32 let mut table = GameEntityCounterTable::default();
33 for entry in counter_map {
34 for (counter_type, amount) in entry.counters {
35 table.put(entry.source, object, counter_type, amount);
36 }
37 }
38 table
39 }
40
41 pub fn put(
42 &mut self,
43 source: Option<PlayerId>,
44 object: GameEntity,
45 counter_type: CounterType,
46 amount: i32,
47 ) {
48 if amount <= 0 {
49 return;
50 }
51 if let Some(cell) = self
52 .cells
53 .iter_mut()
54 .find(|cell| cell.source == source && cell.object == object)
55 {
56 *cell.counters.entry(counter_type).or_default() += amount;
57 } else {
58 self.cells.push(CounterCell {
59 source,
60 object,
61 counters: BTreeMap::from([(counter_type, amount)]),
62 });
63 }
64 }
65
66 pub fn is_empty(&self) -> bool {
67 self.cells.is_empty()
68 }
69
70 fn total_values(&self) -> i32 {
71 self.cells
72 .iter()
73 .flat_map(|cell| cell.counters.values())
74 .sum()
75 }
76
77 pub fn get(
78 &self,
79 source: Option<PlayerId>,
80 object: GameEntity,
81 counter_type: &CounterType,
82 ) -> i32 {
83 self.cells
84 .iter()
85 .find(|cell| cell.source == source && cell.object == object)
86 .and_then(|cell| cell.counters.get(counter_type))
87 .copied()
88 .unwrap_or(0)
89 }
90
91 pub fn counters(game: &GameState, object: GameEntity) -> Vec<(CounterType, i32)> {
92 match object {
93 GameEntity::Card(card) => game
94 .card(card)
95 .counters
96 .iter()
97 .filter(|(_, amount)| **amount > 0)
98 .map(|(counter_type, amount)| (counter_type.clone(), *amount))
99 .collect(),
100 GameEntity::Player(player) => {
101 let player = game.player(player);
102 [
103 (CounterType::Poison, player.poison_counters),
104 (
105 CounterType::Named("ENERGY".to_string()),
106 player.energy_counters,
107 ),
108 (
109 CounterType::Named("RAD".to_string()),
110 player.radiation_counters,
111 ),
112 ]
113 .into_iter()
114 .filter(|(_, amount)| *amount > 0)
115 .collect()
116 }
117 }
118 }
119
120 pub fn replace_counter_effect(
121 &self,
122 game: &mut GameState,
123 trigger_handler: Option<&mut TriggerHandler>,
124 agents: Option<&mut [Box<dyn PlayerAgent>]>,
125 cause: Option<&SpellAbility>,
126 is_effect: bool,
127 params: RunParams,
128 ) -> GameEntityCounterTable {
129 self.apply_counter_effect(
130 game,
131 trigger_handler,
132 agents,
133 cause,
134 is_effect,
135 params,
136 false,
137 )
138 }
139
140 pub fn apply_replaced_counter_effect(
141 &self,
142 game: &mut GameState,
143 trigger_handler: Option<&mut TriggerHandler>,
144 cause: Option<&SpellAbility>,
145 params: RunParams,
146 ) -> GameEntityCounterTable {
147 self.apply_counter_effect(game, trigger_handler, None, cause, true, params, true)
148 }
149
150 fn apply_counter_effect(
151 &self,
152 game: &mut GameState,
153 mut trigger_handler: Option<&mut TriggerHandler>,
154 mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
155 cause: Option<&SpellAbility>,
156 is_effect: bool,
157 params: RunParams,
158 already_replaced: bool,
159 ) -> GameEntityCounterTable {
160 let remember_amount = self.total_values();
161 let mut result = GameEntityCounterTable::default();
162 let mut remembered_objects = Vec::new();
163 let mut objects = Vec::new();
164 for cell in &self.cells {
165 if !objects.contains(&cell.object) {
166 objects.push(cell.object);
167 }
168 }
169
170 for object in objects {
171 let first_time = matches!(object, GameEntity::Card(_))
172 && game.counter_added_this_turn(object, None) == 0;
173 let counter_map: Vec<_> = self
174 .cells
175 .iter()
176 .filter(|cell| cell.object == object)
177 .map(|cell| CounterMapValue {
178 source: cell.source,
179 counters: cell.counters.clone(),
180 })
181 .collect();
182 let Some((source, counter_type, count)) = counter_map.iter().find_map(|entry| {
183 entry
184 .counters
185 .iter()
186 .next()
187 .map(|(counter_type, amount)| (entry.source, counter_type.clone(), *amount))
188 }) else {
189 continue;
190 };
191 let mut event = ReplacementEvent::AddCounter {
192 target: object,
193 source,
194 counter_type,
195 count,
196 counter_map,
197 after_replacement_static_abilities: Vec::new(),
198 cause: cause.cloned().map(Box::new),
199 is_effect,
200 };
201 let replacement_result = if already_replaced {
202 ReplacementResult::NotReplaced
203 } else {
204 match agents.as_deref_mut() {
205 Some(agents) => apply_replacements_with_agents(game, agents, &mut event),
206 None => apply_replacements(game, &mut event),
207 }
208 };
209 if matches!(
210 replacement_result,
211 ReplacementResult::NotReplaced | ReplacementResult::Updated
212 ) {
213 let ReplacementEvent::AddCounter {
214 counter_map,
215 after_replacement_static_abilities,
216 ..
217 } = event
218 else {
219 continue;
220 };
221 for entry in counter_map {
222 for (counter_type, count) in entry.counters {
223 let count = match (object, cause.and_then(|cause| cause.ir.max_from_effect))
224 {
225 (GameEntity::Card(card), Some(max)) => {
226 (max - game.card(card).counter_count(&counter_type)).clamp(0, count)
227 }
228 _ => count,
229 };
230 let added = add_counter_internal(
231 game,
232 trigger_handler.as_deref_mut(),
233 object,
234 entry.source,
235 &counter_type,
236 count,
237 cause,
238 params.clone(),
239 );
240 if added > 0 {
241 result.put(entry.source, object, counter_type, added);
242 if cause.is_some_and(|cause| cause.ir.remember_put)
243 && !remembered_objects.contains(&object)
244 {
245 remembered_objects.push(object);
246 }
247 }
248 }
249 }
250 for (source, static_abilities) in after_replacement_static_abilities {
251 crate::replacement::replace_add_counter::apply_after_replacement_static_abilities(
252 game,
253 source,
254 static_abilities,
255 );
256 }
257 }
258 if result.cells.iter().any(|cell| cell.object == object) {
259 if let Some(handler) = trigger_handler.as_deref_mut() {
260 handler.run_trigger(
261 TriggerType::CounterTypeAddedAll,
262 object_params(
263 object,
264 RunParams {
265 first_time: Some(first_time),
266 ..Default::default()
267 },
268 ),
269 false,
270 );
271 }
272 }
273 }
274
275 if let Some(source) = cause.and_then(|cause| cause.source) {
276 for object in remembered_objects {
277 match object {
278 GameEntity::Card(card) => game.card_mut(source).add_remembered_card(card),
279 GameEntity::Player(player) => {
280 game.card_mut(source).add_remembered_player(player)
281 }
282 }
283 }
284 if cause.is_some_and(|cause| cause.ir.remember_amount) && remember_amount > 0 {
285 game.card_mut(source).add_remembered_cmc(remember_amount);
286 }
287 }
288
289 if let Some(handler) = trigger_handler {
290 result.trigger_counters_put_all(handler);
291 }
292 result
293 }
294
295 fn trigger_counters_put_all(&self, trigger_handler: &mut TriggerHandler) {
296 if self.is_empty() {
297 return;
298 }
299 for cell in &self.cells {
300 let counter_map = counter_map(&cell.counters);
301 trigger_handler.run_trigger(
302 TriggerType::CounterPlayerAddedAll,
303 object_params(
304 cell.object,
305 RunParams {
306 source_player: cell.source,
307 counter_amount: Some(cell.counters.values().sum()),
308 counter_map: Some(counter_map),
309 ..Default::default()
310 },
311 ),
312 false,
313 );
314 }
315 trigger_handler.run_trigger(
316 TriggerType::CounterAddedAll,
317 RunParams {
318 counter_table: Some(self.entries()),
319 ..Default::default()
320 },
321 false,
322 );
323 }
324
325 fn entries(&self) -> Vec<CounterTableEntry> {
326 self.cells
327 .iter()
328 .map(|cell| CounterTableEntry {
329 source: cell.source,
330 object_card: match cell.object {
331 GameEntity::Card(card) => Some(card),
332 GameEntity::Player(_) => None,
333 },
334 object_player: match cell.object {
335 GameEntity::Player(player) => Some(player),
336 GameEntity::Card(_) => None,
337 },
338 counters: counter_map(&cell.counters),
339 })
340 .collect()
341 }
342}
343
344fn add_counter_internal(
345 game: &mut GameState,
346 trigger_handler: Option<&mut TriggerHandler>,
347 object: GameEntity,
348 source: Option<PlayerId>,
349 counter_type: &CounterType,
350 amount: i32,
351 cause: Option<&SpellAbility>,
352 mut params: RunParams,
353) -> i32 {
354 if amount <= 0 || !can_receive_counter(game, object, counter_type) {
355 return 0;
356 }
357 let old_value = counter_count(game, object, counter_type);
358 let amount = match object {
359 GameEntity::Card(card) => {
360 if let Some(max) = crate::staticability::static_ability_max_counter::max_counter(
361 &game.cards,
362 game.card(card),
363 counter_type,
364 ) {
365 (max - old_value).clamp(0, amount)
366 } else {
367 amount
368 }
369 }
370 GameEntity::Player(_) => amount,
371 };
372 if amount <= 0 {
373 return 0;
374 }
375
376 let first_time = game.counter_added_this_turn(object, Some(counter_type)) == 0;
377 match object {
378 GameEntity::Card(card) => game
379 .card_mut(card)
380 .add_counter_internal(counter_type, amount),
381 GameEntity::Player(player) => add_player_counter(game, player, counter_type, amount),
382 }
383 let new_value = counter_count(game, object, counter_type);
384 let added = new_value - old_value;
385 if added <= 0 {
386 return 0;
387 }
388 game.record_counter_added(object, counter_type, added);
389
390 if let Some(trigger_handler) = trigger_handler {
391 params = object_params(object, params);
392 params.source_player = source;
393 params.cause = cause.cloned();
394 params.counter_type = Some(counter_type.to_string());
395 for counter_amount in (old_value + 1)..=new_value {
396 params.counter_amount = Some(counter_amount);
397 trigger_handler.run_trigger(TriggerType::CounterAdded, params.clone(), false);
398 }
399 params.counter_amount = Some(added);
400 params.first_time = match object {
401 GameEntity::Card(_) => Some(first_time),
402 GameEntity::Player(_) => None,
403 };
404 trigger_handler.run_trigger(TriggerType::CounterAddedOnce, params, false);
405 }
406 added
407}
408
409fn can_receive_counter(game: &GameState, object: GameEntity, counter_type: &CounterType) -> bool {
410 match object {
411 GameEntity::Card(card) => {
412 !game.card(card).phased_out
413 && !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
414 &game.cards,
415 game.card(card),
416 counter_type,
417 )
418 }
419 GameEntity::Player(player) => {
420 !game.player(player).has_lost
421 && !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
422 &game.cards,
423 player,
424 counter_type,
425 )
426 }
427 }
428}
429
430fn counter_count(game: &GameState, object: GameEntity, counter_type: &CounterType) -> i32 {
431 match object {
432 GameEntity::Card(card) => game.card(card).counter_count(counter_type),
433 GameEntity::Player(player) => match counter_type {
434 CounterType::Poison => game.player(player).poison_counters,
435 CounterType::Named(name) if name == "ENERGY" => game.player(player).energy_counters,
436 CounterType::Named(name) if name == "RAD" => game.player(player).radiation_counters,
437 _ => 0,
438 },
439 }
440}
441
442fn add_player_counter(
443 game: &mut GameState,
444 player: PlayerId,
445 counter_type: &CounterType,
446 amount: i32,
447) {
448 match counter_type {
449 CounterType::Poison => game.player_add_poison(player, amount),
450 CounterType::Named(name) if name == "ENERGY" => game.player_add_energy(player, amount),
451 CounterType::Named(name) if name == "RAD" => {
452 game.player_mut(player).radiation_counters += amount;
453 }
454 _ => {}
455 }
456}
457
458fn object_params(object: GameEntity, mut params: RunParams) -> RunParams {
459 match object {
460 GameEntity::Card(card) => {
461 params.card = Some(card);
462 params.object_card = Some(card);
463 }
464 GameEntity::Player(player) => {
465 params.player = Some(player);
466 params.object_player = Some(player);
467 }
468 }
469 params
470}
471
472fn counter_map(counters: &BTreeMap<CounterType, i32>) -> BTreeMap<String, i32> {
473 counters
474 .iter()
475 .map(|(counter_type, amount)| (counter_type.to_string(), *amount))
476 .collect()
477}