Skip to main content

manabrew_engine/spellability/
target_choices.rs

1//! Target choices for spell abilities.
2//!
3//! Mirrors Java's `spellability/TargetChoices.java` — a container holding
4//! the actual selected targets for a spell ability.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::ids::{CardId, PlayerId};
11
12/// Targets chosen for a single ability in the SubAbility chain.
13/// Mirrors Java's `TargetChoices` which holds selected targets (cards, players, stack entries).
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct TargetChoices {
16    pub target_player: Option<PlayerId>,
17    #[serde(default)]
18    pub additional_target_players: Vec<PlayerId>,
19    pub target_card: Option<CardId>,
20    /// Zone timestamp captured when `target_card` was chosen.
21    /// Used to preserve object identity across zone changes (CR 400.7).
22    #[serde(default)]
23    pub target_card_zone_timestamp: Option<u64>,
24    /// ID of a targeted stack entry (for Counter effects).
25    pub target_stack_entry: Option<u32>,
26    /// Divided damage/effect allocation per target card.
27    /// Mirrors Java's `TargetChoices.dividedMap`.
28    #[serde(skip)]
29    pub divided_map: HashMap<CardId, i32>,
30}
31
32impl TargetChoices {
33    /// Collect all targeted players for this node.
34    pub fn all_target_players(&self) -> Vec<PlayerId> {
35        let mut players = Vec::new();
36        if let Some(player) = self.target_player {
37            players.push(player);
38        }
39        for &player in &self.additional_target_players {
40            if !players.contains(&player) {
41                players.push(player);
42            }
43        }
44        players
45    }
46
47    /// Collect all targeted cards for this node.
48    pub fn all_target_cards(&self) -> Vec<CardId> {
49        let mut cards = Vec::new();
50        if let Some(card) = self.target_card {
51            cards.push(card);
52        }
53        for &card in self.divided_map.keys() {
54            if !cards.contains(&card) {
55                cards.push(card);
56            }
57        }
58        cards
59    }
60
61    /// Add a target (card and/or player).
62    /// Mirrors Java's `TargetChoices.add(GameObject)`.
63    pub fn add(&mut self, target_card: Option<CardId>, target_player: Option<PlayerId>) {
64        if let Some(card) = target_card {
65            self.target_card = Some(card);
66            // Caller can override with the actual captured timestamp if available.
67            self.target_card_zone_timestamp = None;
68        }
69        if let Some(player) = target_player {
70            if self.target_player.is_none() {
71                self.target_player = Some(player);
72            } else if self.target_player != Some(player)
73                && !self.additional_target_players.contains(&player)
74            {
75                self.additional_target_players.push(player);
76            }
77        }
78    }
79
80    /// Remove a card target.
81    /// Mirrors Java's `TargetChoices.remove(Card)`.
82    pub fn remove(&mut self, card: CardId) {
83        if self.target_card == Some(card) {
84            self.target_card = None;
85            self.target_card_zone_timestamp = None;
86        }
87        self.divided_map.remove(&card);
88    }
89
90    /// Clear all targets.
91    /// Mirrors Java's `TargetChoices.removeAll()`.
92    pub fn remove_all(&mut self) {
93        self.target_card = None;
94        self.target_card_zone_timestamp = None;
95        self.target_player = None;
96        self.additional_target_players.clear();
97        self.target_stack_entry = None;
98        self.divided_map.clear();
99    }
100
101    /// Check if a card is targeted.
102    /// Mirrors Java's `TargetChoices.contains(Card)`.
103    pub fn contains(&self, card: CardId) -> bool {
104        self.target_card == Some(card) || self.divided_map.contains_key(&card)
105    }
106
107    /// Replace one card target with another.
108    /// Mirrors Java's `TargetChoices.replaceTargetCard(Card, Card)`.
109    pub fn replace_target_card(&mut self, old: CardId, new: CardId) {
110        if self.target_card == Some(old) {
111            self.target_card = Some(new);
112            self.target_card_zone_timestamp = None;
113            // Move divided allocation if present
114            if let Some(amount) = self.divided_map.remove(&old) {
115                self.divided_map.insert(new, amount);
116            }
117        } else if let Some(amount) = self.divided_map.remove(&old) {
118            self.divided_map.insert(new, amount);
119        }
120    }
121
122    /// Returns controllers that changed for targeted cards.
123    /// Mirrors Java's `TargetChoices.forEachControllerChanged()`.
124    /// Currently returns an empty vec since controller-change tracking
125    /// is handled at the game state level.
126    pub fn for_each_controller_changed(&self) -> Vec<PlayerId> {
127        Vec::new()
128    }
129
130    /// Add a divided damage/effect allocation for a target card.
131    /// Mirrors Java's `TargetChoices.addDividedAllocation(Card, int)`.
132    pub fn add_divided_allocation(&mut self, card: CardId, amount: i32) {
133        self.divided_map.insert(card, amount);
134    }
135
136    /// Clone this target choices.
137    /// Mirrors Java's `TargetChoices.copy()`.
138    pub fn copy(&self) -> Self {
139        self.clone()
140    }
141}