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